TL;DR 如果你能一眼看出 https://gist.github.com/chenshuo/6430925 中的那 8 個 Waiter classes 哪些是對的哪些是錯的,本文就不必看了。
前幾天,我發(fā)了一條微博 http://weibo.com/1701018393/A7FrW7ZVd ,質(zhì)疑某本書對 Pthreads 條件變量的封裝是錯的,因為它沒有把 mutex 的 lock()/unlock() 函數(shù)暴露出來,導致無法實用。后來大家討論的分歧是這個 cond class 是不是通用的條件變量封裝,還是只是一個特殊的“事件等待器”。作為事件等待器,其實現(xiàn)也是錯的,因為存在丟失事件的可能,可以算是初學者使用條件變量的典型錯誤。
本文的代碼位于 recipes/thread/test/Waiter_test.cc,這里提到的某書的版本相當于 Waiter1 class。
我在拙作《Linux 多線程服務端編程:使用 muduo C++ 網(wǎng)絡庫》第 2.2 節(jié)總結(jié)了條件變量的使用要點:
條件變量只有一種正確使用的方式,幾乎不可能用錯。對于 wait 端:
1. 必須與 mutex 一起使用,該布爾表達式的讀寫需受此 mutex 保護。
2. 在 mutex 已上鎖的時候才能調(diào)用 wait()。
3. 把判斷布爾條件和 wait() 放到 while 循環(huán)中。
對于 signal/broadcast 端:
1. 不一定要在 mutex 已上鎖的情況下調(diào)用 signal (理論上)。
2. 在 signal 之前一般要修改布爾表達式。
3. 修改布爾表達式通常要用 mutex 保護(至少用作 full memory barrier)。
4. 注意區(qū)分 signal 與 broadcast:“broadcast 通常用于表明狀態(tài)變化,signal 通常用于表示資源可用。(broadcast should generally be used to indicate state change rather than resource availability。)”
如果用條件變量來實現(xiàn)一個“事件等待器/Waiter”,正確的做法是怎樣的?我的最終答案見 WaiterInMuduo class。“事件等待器”的一種用途是程序啟動時等待初始化完成,也可以直接用 muduo::CountDownLatch 到達相同的目的,將初值設為 1 即可。
以下根據(jù)微博上的討論過程給出幾個正確或錯誤的版本,博大家一笑。只要記住 Pthread 的條件變量是邊沿觸發(fā)(edge trigger),即 signal()/broadcast() 只會喚醒已經(jīng)等在 wait() 上的線程(s),我們在編碼時必須要考慮 signal() 早于 wait() 的可能,那么就很容易判斷以下各個版本的正誤了。代碼見 recipes/thread/test/Waiter_test.cc。
版本一:錯誤。某書上的原始版,有丟失事件的可能。

版本二:錯誤。lock() 之后再 signal(),同樣有丟失事件的可能。

版本三:錯誤。引入了 bool signaled_; 條件,但沒有正確處理 spurious wakeup。
版本四五六:正確。僅限 single waiter 使用。
版本七:最佳。可供 multiple waiters 使用。
版本八:錯誤。存在 data race,且有丟失事件的可能。理由見 http://stackoverflow.com/questions/4544234/calling-pthread-cond-signal-without-locking-mutex
總結(jié):使用條件變量,調(diào)用 signal() 的時候無法知道是否已經(jīng)有線程等待在 wait() 上。因此一般總是要先修改“條件”,使其為 true,再調(diào)用 signal();這樣 wait 線程先檢查“條件”,只有當條件不成立時才去 wait(),避免了丟事件的可能。換言之,通過使用“條件”,將邊沿觸發(fā)(edge trigger)改為電平觸發(fā)(level trigger)。這里“修改條件”和“檢查條件”都必須在 mutex 保護下進行,而且這個 mutex 必須用于配合 wait()。
思考題:如果用兩個 mutex,一個用于保護“條件”,另一個專門用于和 cond 配合 wait(),會出現(xiàn)什么情況?
最后注明一點,http://stackoverflow.com/questions/6419117/signal-and-unlock-order 這篇帖子里對 spurious wakeup 的解釋是錯的,spurious wakeup 指的是一次 signal() 調(diào)用喚醒兩個或以上 wait()ing 的線程,或者沒有調(diào)用 signal() 卻有線程從 wait() 返回。manpage 里對 Pthreads 系列函數(shù)的介紹非常到位,值得細讀。