最近在看陳碩寫的多線程服務端編程,感嘆真是本好書,寫作嚴謹且內容豐富,沒有一定的功力和多年實戰經驗是寫不出來的,贊一個。書中第二章講到了條件變量,對於這個同步原語,我的了解不多,也沒曾深入去了解,只知道大概就是個用來當信號處理用的東西,以前在多線程方面,一般就 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++ 的類來包裝一層,對新手來說,學習的成本會低些吧。不過對寫代碼來說,求簡單是有代價的,很多時候需要折衷,一直都這樣。