用條件變量實現事件等待器的正確與錯誤做法

原文 :陳碩

TL;DR 如果你能一眼看出 https://gist.github.com/chenshuo/6430925 中的那 8 個 Waiter classes 哪些是對的哪些是錯的,本文就不必看了。

前幾天,我發了一條微博 http://weibo.com/1701018393/A7FrW7ZVd ,質疑某本書對 Pthreads 條件變量的封裝是錯的,因爲它沒有把 mutex 的 lock()/unlock() 函數暴露出來,導致無法實用。後來大家討論的分歧是這個 cond class 是不是通用的條件變量封裝,還是隻是一個特殊的“事件等待器”。作爲事件等待器,其實現也是錯的,因爲存在丟失事件的可能,可以算是初學者使用條件變量的典型錯誤。

本文的代碼位於 recipes/thread/test/Waiter_test.cc,這裏提到的某書的版本相當於 Waiter1 class。

我在拙作《Linux 多線程服務端編程:使用 muduo C++ 網絡庫》第 2.2 節總結了條件變量的使用要點:

條件變量只有一種正確使用的方式,幾乎不可能用錯。對於 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

版本一:錯誤。某書上的原始版,有丟失事件的可能。

1

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

2

版本三:錯誤。引入了 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 系列函數的介紹非常到位,值得細讀。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章