先看看linux下條件變量的api:linux
1 #include <pthread.h> 2 int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 3 int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_wait:These functions atomically release mutex and cause the calling thread to block on the condition variable cond.git
pthread_cond_signal : call unblocks at least one of the threads that are blocked on the specified condition variable cond (if any threads are blocked on cond).github
這裏包括了一個解鎖的操做,會引入一個疑問,爲何wait裏須要互斥器mutex?c#
對於自旋鎖,至關於一直嘗試獲取鎖:api
while (lock(mutex) == false) { } // do something
若是其餘thread一直持有該鎖,會致使本線程一直while搶鎖,浪費CPU作無用功.app
理論上不用一直判斷lock,只須要在lock失敗後判斷mutex是否有變化便可。搶鎖失敗後只要鎖的持有狀態一直沒有改變,那就讓出 CPU 給別的線程先執行好了。ide
這就是互斥鎖:函數
while (lock(mutex) == false) { thread sleep untile lock state change }
操做系統負責線程調度,爲了實現鎖的狀態發生改變時再喚醒,mutex_lock sleep須要操做系統處理,所以pthread_mutex_lock涉及上下文切換,開銷比較大。ui
自旋鎖和互斥鎖都是保證可以排它地訪問被鎖保護的資源。this
不少狀況下,咱們並不須要徹底排他性的佔有某些資源,以生產者消費者爲例:
生產者向Queue中添加元素,消費者從Queue中消費元素,使用互斥鎖mutex用於生產者/消費者Queue同步:
lock(mutex); // mutex 保護對 queue 的操做 while (queue.isEmpty()) { // 隊列爲空時等待 unlock(mutex); // wait, 這裏讓出鎖,讓生產者有機會往 queue 裏安放數據 lock(mutex); } data = queue.pop(); // 至此確定非空,因此能對資源進行操做 unlock(mutex); consume(data); // 在臨界區外作其它處理
這裏的while,至關於又搞出了一個自旋鎖,一直等待queue非空。
有了前面自旋鎖、互斥器的經驗就不難想到:「只要條件沒有發生改變,while 裏就沒有必要再去解鎖、 判斷、條件不成立、加鎖,徹底可讓出 CPU 給別的線程」。不過因爲「條件是否達成」屬於業務邏輯, 操做系統無法管理,須要讓可以做出這一改變的代碼來手動「通知」,好比上面的例子裏就須要在生產者 往 queue 裏 push 後「通知」!queue.isEmpty() 成立。
所以咱們但願把while改爲這種形式:
while (queue.isEmpty()) { 解鎖後等待通知喚醒再加鎖(用來收發通知的東西, lock); }
而通知機制則爲:
觸發通知(用來收發通知的東西); // 通常有兩種方式: // 通知全部在等待的(notifyAll / broadcast) // 通知一個在等待的(notifyOne / signal)
這就是條件變量,它解決的不是互斥,而是等待。
上述的解鎖後等待通知再加鎖,就是
pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
換句話說,pthread_cond_wait本質上包含了三個操做:
pthread_mutex_unlock(mtx);
pthread_cond_just_wait(cv);
pthread_mutex_lock(mtx);
上面三行代碼的並非pthread_cond_wait(cv, mtx)的內聯展開。其中第一行和第二行必須「原子化」,而第三行是能夠分離出去的。那麼爲何第一行和第二行不能分離呢?這是由於必須得保證:
若是線程A先進入wait函數(即便沒有進入實際的等待狀態,好比正在釋放mtx),那麼必須得保證其餘線程在其以後調用的broadcast必須可以將線程A喚醒。
條件變量只有一種正確使用的方式,幾乎不可能用錯。對於 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() 的可能,那麼就很容易判斷如下各個版本的正誤了
總結: 使用條件變量,調用 signal() 的時候沒法知道是否已經有線程等待在 wait() 上。所以通常老是要先修改「條件」,使其爲 true,再調用 signal();這樣 wait 線程先檢查「條件」,只有當條件不成立時纔去 wait(),避免了丟事件的可能。換言之,經過使用「條件」,將邊沿觸發(edge trigger)改成電平觸發(level trigger)。這裏「修改條件」和「檢查條件」都必須在 mutex 保護下進行,並且這個 mutex 必須用於配合 wait()。
tips:
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.
spurious wakeup 指的是一次 signal() 調用喚醒兩個或以上 wait()ing 的線程,或者沒有調用 signal() 卻有線程從 wait() 返回,虛假喚醒。
APUE上這樣說:
POSIX規範爲了簡化實現,容許pthread_cond_signal在實現的時候能夠喚醒不止一個線程。
在發生的spurious wakeup時候,waiting線程被意外的喚醒,而後到真正signal的時候,waiting線程在以前已經spurious wakeup喚醒了。
有幾篇30多年前的論文極大地影響了現代操做系統中進程/線程的同步機制的實現,尤爲是樓主問題中的實現。一篇是 Monitors: An Operating System Structuring Concept ( http://www.vuse.vanderbilt.edu/~dowdy/courses/cs381/monitor.pdf),還有一篇是 Experience with Processes and Monitors in Mesa (http://msr-waypoint.com/en-us/um/people/blampson/23-ProcessesInMesa/Acrobat.pdf)。另外,還有討論semaphore,生產者/消費者問題,哲學家就餐問題等等的論文。
這裏,我先介紹這兩篇論文的內容,而後引出問題的答案。
第一篇是Tony Hoare寫的,一般被叫作Hoare's Monitor。你可能不知道Tony Hoare是誰,可是你應該知道他發明的quicksort。你也應該知道他得過的那個圖靈獎。Monitor是一種用來同步對資源的訪問的機制。Monitor裏有一個或多個過程(procedure),在一個時刻只能有一個過程被激活(active)。讓我來給個例子:MONITOR account { int balance; //initial value is 0; procedure add_one() { balance++ } procedure remove_one() { balance-- } }
若是這是一個monitor,add_one()和remove_one()中只能有一箇中被激活的。也就是說,balance++和balance--不可能同時執行,沒有race。
正如論文的標題所說的,monitor只是一個概念,他能夠被幾乎任何現代的語言實現。若是咱們要用C++來實現Monitor,僞代碼差很少就是這樣(Java的Synchronization是Monitor的一個實現):class Account { private: int balance; //initial value is 0; lock mutex; public: void add_one() { pthread_mutex_lock(&mutex); balance++; pthread_mutex_unlock(&mutex); } void remove_one() { pthread_mutex_lock(&mutex); balance-- pthread_mutex_unlock(&mutex); } };
//注意這是一個monitor,這裏的acquire()和release()不能也不會同時active。 Monitor SingleResource { private: bool busy; condition_variable nonbusy; public: void acquire() { if ( busy == true ) { nonbusy.wait() } busy = true } void release() { busy = false; nonbusy.signal() } busy = false; //initial value of busy };
Since other programs may invoke a monitor procedure during a wait, a waiting program must ensure that the invariant t for the monitor is true beforehand.
換句話說,當線程A等待時,爲了確保其餘線程能夠調用monitor的procedure,線程A在等待前,必須釋放鎖。例如,在使用monitor SingleResource時,線程A調用acquire()並等待。線程A必須在實際睡眠前釋放鎖,要否則,即便線程A已經不active了,線程B也無法調用acquire()。(固然,你也能夠不釋放鎖,另外一個線程根本不檢查鎖的狀態,而後進入對條件變量的等待!! 可是,首先,這已經不是一個monitor了,其次,看下文。)
pthread只是提供了一種同步的機制,你可使用這種機制來作任何事情。有的對,有的錯。Hoare在論文裏的一段話也說更能解答樓主的問題:The introduction of condition variables makes it possible to write monitors subject to the risk of deadly embrace [7]. It is the responsibility of the programmer to avoid this risk, together with other scheduling disasters (thrashing, indefinitely repeated overtaking, etc. [11]).
有興趣的同窗能夠讀讀這篇文章,文中有一節專門解釋了樓主的問題。樓主的問題顯然是很深入的。
if ( busy == true ) {
while ( busy == true ) {
?
這是由於,在Hoare的假設裏,當線程A調用nonbusy.signal()以後,線程A必須當即中止執行,正在等待的線程B必須緊接着當即開始執行。這樣,就能夠確保線程B開始執行時 busy==false。這正是咱們想要的。
可是,在現代的系統中,這個假設並不成立。現代操做系統中的機制跟Mesa中描述的一致:在condvar.signal()被調用以後,正在等待的線程並不須要當即開始執行。等待線程能夠在任何方便的時候恢復執行(優勢之一:這樣就把同步機制和調度機制分開了)。
在Mesa的假設下,上面的Monitor SingleResource的代碼是錯的。試想下面的執行過程:1. 線程A調用acquire()並等待,2. 線程B調用release(),2.線程C調用acquire(),如今 busy=true,3. 線程A恢復執行,可是此時busy已是true了! 這就會致使線程A和線程C同時使用被這個monitor保護的資源!!!void acquire() { if ( busy == true ) { nonbusy.wait() } //assert(busy != true) busy = true }
在Mesa中,Butler Lampson和David Redell提出了一個簡單的解決方案-把 if 改爲 while。這樣的話,在線程A恢復執行時,還要再檢查一下busy的值。若是還不是想要的,就會再次等待。