信號量是內核對象,它允許多個線程在同一個時刻訪問同一個共享資源,但是需要限制在同一時刻訪問此共享資源的最大線程數量。在創建信號量時,要指定允許的最大資源計數和當前可用的資源數。一般將當前可用資源數設置爲最大資源數,每增加一個線程對共享資源的訪問,當前可用資源數減1。當可用資源計數減小到0時,則說明當前佔用資源的線程數達到了所允許的最大數目,其他線程無法再進入,必須等待(阻塞)。佔用資源的線程在處理完成後,會釋放佔用的資源,此時可用資源計數加1,這時等待的線程隊列中,會有其中一個線程被喚醒並獲取資源。
信號量有兩種類型:二進制信號量和計數信號量。
二進制信號量,只有0和1兩種取值。一般用於保護一段代碼使其每次只被一個線程執行,這種信號量可代替互斥鎖來實現對資源的互斥訪問。
計數信號量,可以有更大的取值範圍,允許有限數目的線程共同訪問某個共享資源。
常用的信號量操作有如下一些函數:
信號量的創建
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem即要被創建並初始化的信號量指針;
value用來指定信號量的初始值,也就是共享資源同時可被訪問的最大線程數;
第二個參數pshared需要注意,該參數設置爲0時,表明該信號量是在進程內使用,即實現線程之間的同步,pshared需要定義在所有線程都可見的位置,比如定義成全局變量,或者動態分配在堆上的變量。該參數設置爲非零值時,用於進程之間的同步,這時信號量需要定義在共享內存區域中,任何可以訪問該共享內存區域的進程都可以通過sem_post()和sem_wait()操作該信號量。
本篇我們只討論進程內的同步。
信號量的控制
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
sem_post()會使信號量的值遞增,如果信號量的值大於0,那麼另一個阻塞在sem_wait()上等待該信號的線程就會被喚醒,並獲取共享資源;
sem_wait()會使信號量的值遞減。如果信號量的值大於0,那麼執行遞減操作並立即返回;若信號量的當前值爲0,那麼該調用將會被阻塞,直到信號量的值變爲正值,或者被某個信號處理函數中斷該調用。
sem_trywait(),與sem_wait()作用類似,不同的是當信號量的值爲0而無法執行遞減操作時,返回一個錯誤碼(EAGAIN),而不是阻塞宿主線程的執行。
sem_timedwait(),與sem_wait()作用類似,不同的是設置了一個阻塞的時長abs_timeout,當阻塞時間超過該時長時,返回一個ETIMEDOUT錯誤。
信號量的銷燬
#include <semaphore.h>
int sem_destroy(sem_t *sem);
sem_destroy()用來銷燬一個未命名的信號量,信號量地址由參數sem指定。只有由sem_init()函數初始化的信號量才能調用sem_destroy()銷燬。信號量在銷燬前要確保沒有其他線程或進程在佔用它。一個信號量被銷燬後不可再被使用,否則會產生未知結果,被銷燬的信號量,可通過sem_init()重新初始化後再被使用。
下面我們通過一個例子來理解信號量的工作機制。
假設一個醫院的某科室有三個醫生,每個醫生每次接診一個病人,那麼該科室最多可接診的病人數量爲3。假如一段時間內,該科室僅提供20個掛號名額,也就是說有20人就診,那麼我們如何使用信號量的機制來模擬該科室的就診情況呢?
/* 醫生和病人的例子:3個醫生,20個病人,每次每個醫生服務一個病人,也就是說,最多有3個病人可同時就診 */
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
#include<semaphore.h>
/* 信號量定義爲全局變量 */
sem_t g_sem;
/* 定義一定時間段內,接診的病人總數量 */
const int PATIENT_NUM = 20;
/* 每個線程模擬一個病人的就診 */
void *patient_service(void *arg)
{
int patient_id = *((int *)arg); //輸入參數爲病患ID
if(sem_wait(&g_sem) == 0) //病人等待服務資源,即醫生空閒
{
printf("Patient %d is seeing the doctor. \n", patient_id);
sleep(2); //每個病人的就診時間爲2秒
sem_post(&g_sem); //當前病人服務完成,釋放一個醫生資源
}
return NULL;
}
int main()
{
/* 初始化信號量,指定爲線程之間的共享,信號量值爲3 */
sem_init(&g_sem, 0, 3);
pthread_t patient_thread[PATIENT_NUM]; //每個病人爲一個線程
for(int i = 0; i < PATIENT_NUM; i++)
{
int ret;
int patientID = i;
/* 爲第i個病人創建服務線程 */
ret = pthread_create(&patient_thread[i], NULL, patient_service, &patientID);
if(ret != 0)
{
printf("pthread_create error! \n");
exit(EXIT_FAILURE);
}
usleep(50);
}
/* 連接已終止的線程 */
for(int j = 0; j < PATIENT_NUM; j++)
{
pthread_join(patient_thread[j], NULL);
}
/* 最後釋放信號量 */
sem_destroy(&g_sem);
return 0;
}
執行結果: