最近在看陳碩寫的多執行緒服務端程式設計,感嘆真是本好書,寫作嚴謹且內容豐富,沒有一定的功力和多年實戰經驗是寫不出來的,贊一個。書中第二章講到了條件變數,對於這個同步原語,我的瞭解不多,也沒曾深入去了解,只知道大概就是個用來當訊號處理用的東西,以前在多執行緒方面,一般就 mutex, semaphore 用的多,似乎也能處理大部分的需求了。知識面過窄的鐵血證據。。。於是趕緊順手去查一下。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
乍一看還以為和 semaphore 一個樣,介面看起來彷彿挺像的。。。 啥眼神呢,差遠了!從使用上來說,semaphore 是有狀態的,允許先 post_semaphore,再 wait_semaphore,但 condition variable 就不行了,它是一種無狀態的東西,如果呼叫 pthread_cond_signal() 在前,pthread_cond_wait() 在後,那麼 wait 是需要 block 住等待下一個 signal 的,因為前面的 signal 已經丟了。
怎樣正確使用條件變數
陳碩在書中以絕對肯定的口吻指出正確使用條件變數的方式只有一種,按這方式的來套是“幾乎不可能用錯”的。論斷很有意思,莫非條件變數很容易用錯嗎?再回頭細查一下pthread_cond_wait 的 manual,發現這貨用起來確實夠麻煩的,不但稀奇古怪的結合了一個mutex,還要防止掉各種坑,比如說 spurious wakeup。 那什麼是spurious wakeup呢,書中沒有展開,估計預設是常識,我到網上查了下,spurious wakeup 說的是這樣一種行為:
"a thread might be awoken from its waiting state even though no thread signaled the condition variable”
上面這段話摘自 wikipedia,可以這樣理解,pthread_cond_wait 返回後,並不一定就真的是因為別的地方呼叫了pthread_cond_signal(),有可能是因為別的原因而返回了,因此這個 wakeup 是假的(spurious),在 manual 中也有說明:
"When using condition variables there is always a boolean predicate involving shared variables associated with each condition wait that is true if the thread should proceed. Spurious wakeups from the pthread_cond_wait() or pthread_cond_timedwait() functions may occur. Since the return from pthread_cond_wait() or pthread_cond_timedwait() does not imply anything about the value of this predicate, the predicate should be re-evaluated upon such return."
結論是: 條件變數應該始終結合一個 bool 變數來使用,這個 bool 變數用來指示是否真的有人呼叫了signal,從而解決 spurious wakeup 的問題。因此,按書中的說法,正確使用條件變數的方法有且只有下面這一種:
對於 wait 端:
1) 必須與mutex一起使用,且相應的bool變數要受該mutex的保護。
2) 先lock mutex,再wait。
3) 且,wait()要放到迴圈中,直到bool變數已改變。
對於signal 端:
1) signal()呼叫可以不用mutex保護。
2) 要先修改bool變數再進行signal().
3) 修改該bool變數需要用mutex進行保護。
寫成程式碼的話,大概如下:
1 bool signaled = false;
2 pthread_mutex_t g_mutex;
3 pthread_cond_t g_cond;
4
5 void wait()
6 {
7 pthread_mutex_lock(&g_mutex);
8 while (!g_signaled)
9 {
10 pthread_cond_wait(&g_cond, &g_mutex);
11 }
12 //reset g_signaled if necessary.
13 //g_signaled = false;
14 pthread_mutex_unlock(&g_mutex);
15 }
16
17 void signal()
18 {
19 pthread_mutex_lock(&g_mutex);
20 g_signaled = true;
21 pthread_mutex_unlock(&g_mutex);
22 pthread_cond_signal(&g_cond);
23 }
Spurious wakeup的起因
根據前面的討論,條件變數的程式碼寫的這麼麻煩,似乎完全就是因為這個spurious wakeup,那這個問題究竟是怎麼引起的呢?為什麼會有這樣的問題呢?stackoverflow上有過一個討論:http://stackoverflow.com/questions/8594591/why-does-pthread-cond-wait-have-spurious-wakeups
結論是:
"Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations."
也就是說,不處理 spurious wakeup 使得條件變數的實現效率更高而且更容易實現,這看起來倒和有些長系統呼叫會被中斷有些類似。如下程式碼來自這裡,實現了一個簡單的 wait 和 signal,解釋了為什麼會有 spurious wakeup,程式碼中的 wait,signal 分別在兩個執行緒中進行,執行的順序按最右邊的序號進行。
pthread_cond_wait(mutex, cond):
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 10 */
if (value == cond->value) { /* 11 */
me->next_cond = cond->waiter;
cond->waiter = me;
pthread_mutex_unlock(cond->mutex);
unable_to_run(me);
} else
pthread_mutex_unlock(cond->mutex); /* 12 */
pthread_mutex_lock(mutex); /* 13 */
pthread_cond_signal(cond):
pthread_mutex_lock(cond->mutex); /* 3 */
cond->value++; /* 4 */
if (cond->waiter) { /* 5 */
sleeper = cond->waiter; /* 6 */
cond->waiter = sleeper->next_cond; /* 7 */
able_to_run(sleeper); /* 8 */
}
pthread_mutex_unlock(cond->mutex); /* 9 */
顯然,如果呼叫 wait 的執行緒在第10步那裡卡住了且之前已經有執行緒在等著 signal 的時候, 這時一旦有人去 signal,必然就會有多個執行緒同時被喚醒了,顯然那些被卡在第10步那裡的執行緒就是spurious wakeup了。說實話,確實有些不好用,也不夠直接,如果能夠用 c++ 的類來包裝一層,對新手來說,學習的成本會低些吧。不過對寫程式碼來說,求簡單是有代價的,很多時候需要折衷,一直都這樣。