前言
在多線程編程中,互斥鎖與條件變量恐怕是最常用也是最實用的線程同步原語。
關於條件變量一共也就pthread_cond_init、pthread_cond_destroy、pthread_cond_wait、pthread_cond_timedwait、pthread_cond_signal、pthread_cond_broadcast這么幾個函數,但是在實際使用中卻是很容易用錯,后文將來分析幾種常見使用情況的正確性。
分析
下面是一個輔助基類、便於減少篇幅(為了簡單起見,后文中的所有函數調用並未檢查返回的錯誤情況):
class ConditionBase { public: ConditionBase() { pthread_mutex_init(&mutex_, NULL); pthread_cond_init(&cond_, NULL); } ~ConditionBase() { pthread_mutex_destroy(&mutex_); pthread_cond_destroy(&cond_); } private: pthread_mutex_t mutex_; pthread_cond_t cond_; };
版本一
class Condition1 : public ConditionBase { public: void wait() { pthread_mutex_lock(&mutex_); pthread_cond_wait(&cond_, &mutex_); pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_cond_signal(&cond_); } };
錯誤,這種情況有可能丟失事件。當signal發生在wait之前,就會丟失這次signal事件。如下圖
版本二
class Condition2 : public ConditionBase { public: void wait() { pthread_mutex_lock(&mutex_); pthread_cond_wait(&cond_, &mutex_); pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_mutex_lock(&mutex_); pthread_cond_signal(&cond_); pthread_mutex_unlock(&mutex_); } };
錯誤,同情況一一樣有可能丟失事件。當signal事件發生在wait之前就會丟失signal事件。如下圖
版本三
class Condition3 : public ConditionBase { public: Condition3() : signal_(false) { } void wait() { pthread_mutex_lock(&mutex_); if (!signal_) { pthread_cond_wait(&cond_, &mutex_); } signal_ = false; pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_mutex_lock(&mutex_); signal_ = true; pthread_cond_signal(&cond_); pthread_mutex_unlock(&mutex_); } private: bool signal_; };
錯誤。引入了bool變量來檢查狀態,但是遇到spurious wakeup仍然會發生錯誤。
什么是spurious wakeup?Wikipedia中是這樣說的:
Spurious wakeup describes a complication in the use of condition variables as provided by certain multithreading APIs such as POSIX Threads and the Windows API. Even after a condition variable appears to have been signaled from a waiting thread's point of view, the condition that was awaited may still be false. One of the reasons for this is a spurious wakeup; that is, a thread might be awoken from its waiting state even though no thread signaled the condition variable.
也就是說一次signal調用喚醒了2個或者2個以上的waiting中的線程,這種現象就是spurious wakeup,虛假喚醒。
APUE上這樣說:
POSIX規范為了簡化實現,允許pthread_cond_signal在實現的時候可以喚醒不止一個線程。
在發生的spurious wakeup時候,waiting線程被意外的喚醒,然后到真正signal的時候,waiting線程在之前已經spurious wakeup喚醒了,這樣就會造成不易debug的錯誤。如下圖
版本四
class Condition4 : public ConditionBase { public: Condition4() : signal_(false) { } void wait() { pthread_mutex_lock(&mutex_); while (!signal_) { pthread_cond_wait(&cond_, &mutex_); } signal_ = false; pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_mutex_lock(&mutex_); signal_ = true; pthread_cond_signal(&cond_); pthread_mutex_unlock(&mutex_); } private: bool signal_; };
正確。這個是推薦用法,APUE, UNP,man手冊中都是這種用法,在wait上用while循環而不是if就可以正確處理spurious wakeup情況了。當發生spurious wakeup時,wait被意料之外的喚醒,但是循環條件並沒有改變,於是循環繼續執行pthread_cond_wait,然后繼續進入wait,等待被正確的喚醒。
版本五
class Condition5 : public ConditionBase { public: Condition5() : signal_(false) { } void wait() { pthread_mutex_lock(&mutex_); while (!signal_) { pthread_cond_wait(&cond_, &mutex_); } signal_ = false; pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_mutex_lock(&mutex_); signal_ = true; pthread_mutex_unlock(&mutex_); pthread_cond_signal(&cond_); } private: bool signal_; };
正確。版本五與版本四的唯一區別就是在喚醒的時候先解鎖再調用signal發起喚醒。這么做不會有錯誤,但是可能有較大幾率會使線程調度不在最理想狀態,例如在wakeup調用中的解鎖以后,調用signal以前,系統調度發生線程切換,使得signal沒有在第一時間被發出。
這是在stackoverflow的一個帖子中的說法:http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex
Note that you can actually move the
pthread_cond_signal()
itself after thepthread_mutex_unlock()
, but this can result in less optimal scheduling of threads, and you've necessarily locked the mutex already in this code path due to changing the condition itself.
版本六
class Condition6 : public ConditionBase { public: Condition6() : signal_(false) { } void wait() { pthread_mutex_lock(&mutex_); while (!signal_) { pthread_cond_wait(&cond_, &mutex_); } signal_ = false; pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_mutex_lock(&mutex_); pthread_cond_signal(&cond_); signal_ = true; pthread_mutex_unlock(&mutex_); } private: bool signal_; };
正確,版本六與版本四的區別狀態的改變和發起signal喚醒信號的順序互換了,由於整個wakeup過程都在mutex的包含之下,所以並沒有影響。但是個人更推薦版本四,因為更符合邏輯,不然APUE和UNP也不會都用版本四的寫法順序了:)
版本七
class Condition7 : public ConditionBase { public: Condition7() : signal_(false) { } void wait() { pthread_mutex_lock(&mutex_); while (!signal_) { pthread_cond_wait(&cond_, &mutex_); } signal_ = false; pthread_mutex_unlock(&mutex_); } void wakeup() { pthread_mutex_lock(&mutex_); signal_ = true; pthread_cond_broadcast(&cond_); pthread_mutex_unlock(&mutex_); } private: bool signal_; };
正確。與版本四的區別是這里用了broadcast,而不是signal。對於這種只有一個wait線程的時候,是沒有問題的。但是當有多個wait線程的時候,使用broadcast就把所有wait線程都喚醒了。
另外,當我們使用條件變量cond實現事件等待器的時候,就要用broadcast而不是signal了,因為當有多個事件掛起在wait調用上等待時,signal只能喚醒其中的一個等待線程,並且我們不能期待它喚醒具體的某一個線程,因為這個是不可控的。
版本八
class Condition8 : public ConditionBase { public: Condition8() : signal_(false) { } void wait() { pthread_mutex_lock(&mutex_); while (!signal_) { pthread_cond_wait(&cond_, &mutex_); } signal_ = false; pthread_mutex_unlock(&mutex_); } void wakeup() { signal_ = true; pthread_cond_signal(&cond_); } private: bool signal_; };
錯誤。存在data race,從而導致有可能丟失事件。當wakeup調用發生在wait調用中的進入while循環之后,調用pthread_cond_wait之前,就會丟失signal事件。如下圖
另外,在wait調用中,必須用一個mutex同時保護條件狀態和cond的pthread_cond_wait的調用,而不能用2個mutex,一個保護條件狀態,一個保護pthread_cond_wait,pthread_cond_signal的調用。
這樣仍然會出現race condition。比如先發生wait調用,保護條件的mutex加鎖、檢查條件、解鎖,然后切換線程調用wakeup發送signal信號,再切回wait線程,保護cond的mutex加鎖、進入pthread_cond_wait的waiting狀態中,從而丟失了之前的signal事件。
總結
使用條件變量,調用signal/broadcast的時候,無法知道是否已經有線程等在wait上了。因此,一般要先改變條件狀態,然后再發送signal/broadcast信號。然后在wait調用線程上先檢查條件狀態,只有當條件狀態為假的時候才進入pthread_cond_wait進行等待,從而防止丟失signal/broadcast事件。並且檢查條件、pthread_cond_wait,修改條件、signal/broadcast都要在同一個mutex的保護下進行。
參考文獻
- W.Richard Stevens. UNIX環境高級編程(第3版), 人民郵電出版社, 2014
- Wikipedia. Spurious_wakeup
- stackoverflow. Calling pthread_cond_signal without locking mutex
(完)