1. 概述
條件變量(condition variable)是利用共享的變量進行線程之間同步的一種機制。典型的場景包括生產者-消費者模型,線程池實現等。
對條件變量的使用包括兩個動作:
1) 線程等待某個條件, 條件為真則繼續執行,條件為假則將自己掛起(避免busy wait,節省CPU資源);
2) 線程執行某些處理之后,條件成立;則通知等待該條件的線程繼續執行。
3) 為了防止race-condition,條件變量總是和互斥鎖變量mutex結合在一起使用。
一般的編程模式:
- var mutex;
- var cond;
- var something;
- Thread1: (等待線程)
- lock(mutex);
- while( something not true ){
- condition_wait( cond, mutex);
- }
- do(something);
- unlock(mutex);
- //============================
- Thread2: (解鎖線程)
- do(something);
- ....
- something = true;
- unlock(mutex);
- condition_signal(cond);
函數說明:
(1) Condition_wait():調用時當前線程立即進入睡眠狀態,同時互斥變量mutex解鎖(這兩步操作是原子的,不可分割),以便其它線程能進入臨界區修改變量。
(2) Condition_signal(): 線程調用此函數后,除了當前線程繼續往下執行以外; 操作系統同時做如下動作:從condition_wait()中進入睡眠的線程中選一個線程喚醒, 同時被喚醒的線程試圖鎖(lock)住互斥量mutex, 當成功鎖住后,線程就從condition_wait()中成功返回了。
2. 函數接口
- pthread: pthread_cond_wait/pthread_cond_signal/pthread_cond_broadcast()
- Java: Condition.await()/Condition.signal()/Condition.signalAll()
3. 虛假喚醒(spurious wakeup)在采用條件等待時,我們使用的是
- while(條件不滿足){
- condition_wait(cond, mutex);
- }
- 而不是:
- If( 條件不滿足 ){
- Condition_wait(cond,mutex);
- }
這是因為可能會存在虛假喚醒”spurious wakeup”的情況。
也就是說,即使沒有線程調用condition_signal, 原先調用condition_wait的函數也可能會返回。此時線程被喚醒了,但是條件並不滿足,這個時候如果不對條件進行檢查而往下執行,就可能會導致后續的處理出現錯誤。
虛假喚醒在linux的多處理器系統中/在程序接收到信號時可能回發生。在Windows系統和JAVA虛擬機上也存在。在系統設計時應該可以避免虛假喚醒,但是這會影響條件變量的執行效率,而既然通過while循環就能避免虛假喚醒造成的錯誤,因此程序的邏輯就變成了while循環的情況。
注意:即使是虛假喚醒的情況,線程也是在成功鎖住mutex后才能從condition_wait()中返回。即使存在多個線程被虛假喚醒,但是也只能是一個線程一個線程的順序執行,也即:lock(mutex) 檢查/處理 condition_wai()或者unlock(mutex)來解鎖.
4. 解鎖和等待轉移(wait morphing)
解鎖互斥量mutex和發出喚醒信號condition_signal是兩個單獨的操作,那么就存在一個順序的問題。誰先隨后可能會產生不同的結果。如下:
[color=red](1) 按照 unlock(mutex); condition_signal()順序, 當等待的線程被喚醒時,因為mutex已經解鎖,因此被喚醒的線程很容易就鎖住了mutex然后從conditon_wait()中返回了。
- //...
- unlock(mutex);
- condition_signal(cond);
(2) 按照 condition_signal(); unlock(mutext)順序,當等待線程被喚醒時,它試圖鎖住mutex,但是如果此時mutex還未解鎖,則線程又進入睡眠,mutex成功解鎖后,此線程在再次被喚醒並鎖住mutex,從而從condition_wait()中返回。
- //...
- condition_signal(cond);
- unlock(mutex);
[/color]
可以看到,按照(2)的順序,對等待線程可能會發生2次的上下文切換,嚴重影響性能。因此在后來的實現中,對(2)的情況,如果線程被喚醒但是不能鎖住mutex,則線程被轉移(morphing)到互斥量mutex的等待隊列中,避免了上下文的切換造成的開銷。 -- wait morphing
編程時,推薦采用(1)的順序解鎖和發喚醒信號。而Java編程只能按照(2)的順序,否則發生異常!!。
在SUSv3http://en.wikipedia.org/wiki/Single_UNIX_Specification的規范中(pthread),指明了這兩種順序不管采用哪種,其實現效果都是一樣的。
看過apue大家都知道互斥器用於排他性的訪問共享數據而不是等待原語,如果需要等待某個條件發生需要用條件變量。而當用條件變量的時候需要檢查某個布爾表達式是否為真,進行這項檢查的時候需要互斥器來保護,所以此時互斥器和條件變量聯合起來用於同步。
互斥器和條件變量用法如下:
pthread_mutex_lock(&lock);
while (condition_is_false) {
pthread_cond_wait(&cond, &lock);
}
上面那個while能換成if嗎?答案是不能,否則會導致spurious wakeup虛假喚醒。因為不僅要在pthread_cond_wait前要檢查條件是否成立,在pthread_cond_wait之后也要檢查。因為pthread_cond_wait不僅能被pthread_cond_signal/pthread_cond_broadcast喚醒,而且還會被其它信號喚醒,后者就是虛假喚醒。
linux的pthread_cond_wait是用futex系統調用,這個是慢速系統調用,看過apue知道任何慢速系統調用被信號打斷的時候會返回-1,並且把errno置為EINTR,如果慢速系統調用的重啟功能被關閉,需要在調用該系統調用的地方手動重啟它,像下面這樣:
while (1) {
int ret = syscall();
if (ret < 0 && errno == EINTR)
continue;
else
break;
}
但是futex不能這么用,因為futex結束后到再次重啟這個過程有個時間窗,在這個窗口內可能發生了pthread_cond_signal/phread_cond_broadcast,如果發生這種情況,再進行pthread_cond_wait的時候就錯過了一次條件變量的變化,就會無限等待下去。但是如果不像上面那樣寫又無法重啟futex系統調用,咋整呢?這就回到了上面檢查布爾條件的時候為什么用while而不用if。
用while不會因為虛假喚醒而錯過phread_cond_signal/pthread_cond_broadcast,而且在通過判斷while條件不成立檢測出此次喚醒為虛假喚醒並繼續調用futex繼續等待。