訊號量是學習同步的乙個好方式,但是它們實際上並沒有像互斥體和條件變數一樣被廣泛使用。
儘管如此,還是有一些同步問題可以用訊號量簡單解決,產生顯然更加合適的解決方案。
這一章展示了c語言用於處理訊號量的api,以及我用於使它更加容易使用的**。而且它展示了乙個終極挑戰:你能不能使用互斥體和條件變數來實現乙個訊號量?
這一章的**在本書倉庫的semaphore目錄中。
訊號量是用於使執行緒協同工作而不互相影響的資料結構。
posix標準規定了訊號量的介面,它並不是pthread的一部分,但是多數實現pthread的unix系統也實現了訊號量。
posix訊號量的型別是sem_t。這個型別表現為結構體,所以如果你將它賦值給乙個變數,你會得到它的內容副本。複製訊號量完全是乙個壞行為,在posix中,它的複製行為是未定義的。
幸運的是,包裝sem_t使之更安全並易於使用相當容易。我的包裝api在sem.h中:
typedef sem_t semaphore;
semaphore *make_semaphore(int value);
void semaphore_wait(semaphore *sem);
void semaphore_signal(semaphore *sem);semaphore是sem_t的同義詞,但是我認為它更加可讀,而且大寫的首字母會提醒我將它當做物件並使用指標傳遞它。
這些函式的實現在sem.c中:
semaphore *make_semaphore(int value)
make_semaphore接收訊號量的初始值作為引數。它為訊號量分配空間,將訊號量初始化,之後返回指向semaphore的指標。
如果執行成功,sem_init返回0;如果有任何錯誤,它返回-1。使用包裝函式的乙個好處就是你可以封裝錯誤檢查**,這會使使用這些函式的**更加易讀。
下面是semaphore_wait的實現:
void semaphore_wait(semaphore *sem)
下面是semaphore_signal:
void semaphore_signal(semaphore *sem)
我更喜歡把這個這個操作叫做「signal」而不是「post」,雖然它們是乙個意思(發射)。
譯者注:如果你習慣了互斥體(鎖)的操作,也可以改成lock和unlock。互斥體其實就是訊號量容量為1時的特殊形態。
下面是乙個例子,展示了如何將訊號量用作互斥體:
semaphore *mutex = make_semaphore(1);
semaphore_wait(mutex);
// protected code goes here
semaphore_signal(mutex);當你將訊號量用作互斥體時,通常需要將它初始化為1,來表示互斥體是未鎖的。也就是說,只有乙個執行緒可以通過訊號量而不被阻塞。
這裡我使用了變數名稱mutex來表明訊號量被用作互斥體。但是要記住訊號量的行為和pthread互斥體不完全相同。
使用這些訊號量的包裝函式,我們可以編寫出生產者-消費者問題的解決方案。這一節的**在queue_sem.c。
下面是queue的乙個新定義,使用訊號量來代替互斥體和條件變數:
typedef struct queue;下面是make_queue的新版本:
queue *make_queue(int length)
mutex用於確保佇列的互斥訪問,初始值為1,說明互斥體最開始是未鎖的。
item是佇列中物品的數量,它也是可非阻塞執行queue_pop的消費者執行緒的數量。最開始佇列中沒有任何物品。
spaces是佇列中剩餘空間的數量,也是可非阻塞執行queue_push的執行緒數量。最開始的空間數量就是佇列的容量length - 1。
下面是queue_push的新版本,它由生產者執行緒呼叫:
void queue_push(queue *queue, int item) 要注意queue_push並不需要呼叫queue_full,因為訊號量跟蹤了有多少空間可用,並且在佇列滿了的時候阻塞住生產者。
下面是queue_pop的新版本:
int queue_pop(queue *queue) 這個解決方案在《the little book of semaphores》中的第四章以偽**解釋。
為了使用本書倉庫的**,你需要編譯並執行這個解決方案,你應該執行:
$ make queue_sem
$ ./queue_sem任何可以使用訊號量解決的問題也可以使用條件變數和互斥體來解決。乙個證明方法就是可以使用條件變數和互斥體來實現訊號量。
在你繼續之前,你可能想要將其做為乙個練習:編寫函式,使用條件變數和互斥體實現sem.h中的訊號量api。你可以將你的解決方案放到本書倉庫的mysem.c和mysem.h中,你會在mysem_soln.c和mysem_soln.h中找到我的解決方案。
如果你在開始時遇到了麻煩,你可以使用下面**於我的**的結構體定義,作為提示:
typedef struct semaphore;value是訊號量的值。wakeups記錄了掛起訊號的數量,也就是說它是已被喚醒但是還沒有恢復執行的執行緒數量。wakeups的原因是確保我們的訊號量擁有《the little book of semaphores》中描述的性質3。
mutex提供了value和wakeups的互斥訪問,cond是執行緒在需要等待訊號量時所等待的條件變數。
下面是這個結構體的初始化**:
semaphore *make_semaphore(int value)
下面是我使用posix互斥體和條件變數的訊號量實現:
void semaphore_wait(semaphore *semaphore)
while (semaphore->wakeups < 1);
semaphore->wakeups--;
} mutex_unlock(semaphore->mutex);
}當執行緒等待訊號量時,需要在減少value之前對互斥體加鎖。如果訊號量的值為負,執行緒會被阻塞直到wakeups可用。要注意當它被阻塞時,互斥體是未鎖的,所以其它執行緒可以向條件變數傳送訊號。
semaphore_signal的**如下:
void semaphore_signal(semaphore *semaphore)
mutex_unlock(semaphore->mutex);
}同樣,執行緒在增加value之前需要對互斥體加鎖。如果訊號量是負的,說明還有等待執行緒,所以傳送執行緒需要增加wakeups並向條件變數傳送訊號。
此時等待執行緒可能會喚醒,但是互斥體仍然會鎖住它們,直到傳送執行緒解鎖了它。
這個時候,某個等待執行緒從cond_wait中返回,之後檢查是否wakeup仍然有效。如果沒有它會迴圈並再次等待條件變數。如果有效,它會減少wakeup,解鎖互斥體並退出。
這個解決方案使用do-while迴圈的原因可能並不是很明顯。你知道為什麼不使用更普遍的while迴圈嗎?會出現什麼問題呢?
問題就是while迴圈的實現不滿足性質3。乙個傳送執行緒可以在之後的執行中收到它自己的訊號。
使用do-while迴圈,就確保[1]了當乙個執行緒傳送訊號時,另乙個等待執行緒會收到訊號,即使傳送執行緒在某個等待執行緒恢復之前繼續執行並對互斥體加鎖。
C語言 第十一章第十二章
定義 字串是以空字元為結尾的char型別陣列。1.字串的輸入 gets 函式 它區別於getchar 函式,getchar 是輸入單個字元,gets 是輸入字串。區別於scanf s a 函式,scanf 只能讀取乙個單詞,gets 讀取整行輸入,直到遇到換行符。gets 會在顯示的字串末尾自動加上...
C 程式語言 第十一章 運算子過載
1 運算子函式 運算子函式的名字是由關鍵字operator後跟對應的運算子構成。二元運算子可以定義為取乙個引數的非靜態成員函式,也可以定義為去兩個引數的非成員函式。乙個運算子函式必須或者是乙個成員函式,或者至少有乙個使用者定義型別的引數。不存在運算子遮蔽,這就保證了內部運算子可以用,並且為運算子定義...
C語言學習與總結 第十一章 列舉(enum)
所謂列舉是指將變數的值一一枚舉出來,變數只限於列舉出來的值的範圍內取值。宣告列舉型別的一般形式為 enum 列舉型別名 例如 enum week 注意 1 列舉型是乙個集合,集合中的元素 列舉成員 是一些命名的整型常量,元素之間用逗號,隔開。2 day是乙個識別符號,可以看成這個集合的名字,是乙個可...