TL;DR 若是你能一眼看出 https://gist.github.com/chenshuo/6430925 中的那 8 個 Waiter classes 哪些是對的哪些是錯的,本文就沒必要看了。git
前幾天,我發了一條微博 http://weibo.com/1701018393/A7FrW7ZVd ,質疑某本書對 Pthreads 條件變量的封裝是錯的,由於它沒有把 mutex 的 lock()/unlock() 函數暴露出來,致使沒法實用。後來你們討論的分歧是這個 cond class 是否是通用的條件變量封裝,仍是隻是一個特殊的「事件等待器」。做爲事件等待器,其實現也是錯的,由於存在丟失事件的可能,能夠算是初學者使用條件變量的典型錯誤。github
本文的代碼位於 recipes/thread/test/Waiter_test.cc,這裏提到的某書的版本至關於 Waiter1 class。編程
我在拙做《Linux 多線程服務端編程:使用 muduo C++ 網絡庫》第 2.2 節總結了條件變量的使用要點:c#
條件變量只有一種正確使用的方式,幾乎不可能用錯。對於 wait 端:
1. 必須與 mutex 一塊兒使用,該布爾表達式的讀寫需受此 mutex 保護。
2. 在 mutex 已上鎖的時候才能調用 wait()。
3. 把判斷布爾條件和 wait() 放到 while 循環中。網絡
對於 signal/broadcast 端:
1. 不必定要在 mutex 已上鎖的狀況下調用 signal (理論上)。
2. 在 signal 以前通常要修改布爾表達式。
3. 修改布爾表達式一般要用 mutex 保護(至少用做 full memory barrier)。
4. 注意區分 signal 與 broadcast:「broadcast 一般用於代表狀態變化,signal 一般用於表示資源可用。(broadcast should generally be used to indicate state change rather than resource availability。)」多線程
若是用條件變量來實現一個「事件等待器/Waiter」,正確的作法是怎樣的?個人最終答案見 WaiterInMuduo class。「事件等待器」的一種用途是程序啓動時等待初始化完成,也能夠直接用 muduo::CountDownLatch 到達相同的目的,將初值設爲 1 便可。函數
如下根據微博上的討論過程給出幾個正確或錯誤的版本,博你們一笑。只要記住 Pthread 的條件變量是邊沿觸發(edge trigger),即 signal()/broadcast() 只會喚醒已經等在 wait() 上的線程(s),咱們在編碼時必需要考慮 signal() 早於 wait() 的可能,那麼就很容易判斷如下各個版本的正誤了。代碼見 recipes/thread/test/Waiter_test.cc。編碼
版本一:錯誤。某書上的原始版,有丟失事件的可能。spa
版本二:錯誤。lock() 以後再 signal(),一樣有丟失事件的可能。線程
版本三:錯誤。引入了 bool signaled_; 條件,但沒有正確處理 spurious wakeup。
版本四五六:正確。僅限 single waiter 使用。
版本七:最佳。可供 multiple waiters 使用。
版本八:錯誤。存在 data race,且有丟失事件的可能。理由見 http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex
總結:使用條件變量,調用 signal() 的時候沒法知道是否已經有線程等待在 wait() 上。所以通常老是要先修改「條件」,使其爲 true,再調用 signal();這樣 wait 線程先檢查「條件」,只有當條件不成立時纔去 wait(),避免了丟事件的可能。換言之,經過使用「條件」,將邊沿觸發(edge trigger)改成電平觸發(level trigger)。這裏「修改條件」和「檢查條件」都必須在 mutex 保護下進行,並且這個 mutex 必須用於配合 wait()。
思考題:若是用兩個 mutex,一個用於保護「條件」,另外一個專門用於和 cond 配合 wait(),會出現什麼狀況?
最後註明一點,http://stackoverflow.com/questions/6419117/signal-and-unlock-order 這篇帖子裏對 spurious wakeup 的解釋是錯的,spurious wakeup 指的是一次 signal() 調用喚醒兩個或以上 wait()ing 的線程,或者沒有調用 signal() 卻有線程從 wait() 返回。manpage 裏對 Pthreads 系列函數的介紹很是到位,值得細讀。