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

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

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