多線程編程中條件變量和的spurious wakeup 虛假喚醒


1. 概述 
條件變量(condition variable)是利用共享的變量進行線程之間同步的一種機制。典型的場景包括生產者-消費者模型,線程池實現等。 
對條件變量的使用包括兩個動作: 
1) 線程等待某個條件, 條件為真則繼續執行,條件為假則將自己掛起(避免busy wait,節省CPU資源); 
2) 線程執行某些處理之后,條件成立;則通知等待該條件的線程繼續執行。 
3) 為了防止race-condition,條件變量總是和互斥鎖變量mutex結合在一起使用。 
一般的編程模式: 

C++代碼   收藏代碼
  1. var mutex;  
  2. var cond;  
  3. var something;  
  4.   
  5. Thread1: (等待線程)  
  6. lock(mutex);  
  7. while( something not true ){  
  8.     condition_wait( cond, mutex);  
  9. }  
  10. do(something);  
  11. unlock(mutex);  
  12.   
  13. //============================  
  14.   
  15. Thread2: (解鎖線程)  
  16.   
  17. do(something);  
  18. ....  
  19. something = true;  
  20.   
  21. unlock(mutex);  
  22. condition_signal(cond);  


函數說明: 
(1) Condition_wait():調用時當前線程立即進入睡眠狀態,同時互斥變量mutex解鎖(這兩步操作是原子的,不可分割),以便其它線程能進入臨界區修改變量。 
(2) Condition_signal(): 線程調用此函數后,除了當前線程繼續往下執行以外; 操作系統同時做如下動作:從condition_wait()中進入睡眠的線程中選一個線程喚醒, 同時被喚醒的線程試圖鎖(lock)住互斥量mutex, 當成功鎖住后,線程就從condition_wait()中成功返回了。 

2. 函數接口 

C代碼   收藏代碼
  1. pthread: pthread_cond_wait/pthread_cond_signal/pthread_cond_broadcast()  
  2. Java: Condition.await()/Condition.signal()/Condition.signalAll()  



3. 虛假喚醒(spurious wakeup)在采用條件等待時,我們使用的是 

Java代碼   收藏代碼
  1. while(條件不滿足){  
  2.    condition_wait(cond, mutex);  
  3. }  
  4. 而不是:  
  5. If( 條件不滿足 ){  
  6.    Condition_wait(cond,mutex);  
  7. }   



這是因為可能會存在虛假喚醒”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()中返回了。 

C代碼   收藏代碼
  1. //...  
  2. unlock(mutex);    
  3. condition_signal(cond);  



(2) 按照 condition_signal(); unlock(mutext)順序,當等待線程被喚醒時,它試圖鎖住mutex,但是如果此時mutex還未解鎖,則線程又進入睡眠,mutex成功解鎖后,此線程在再次被喚醒並鎖住mutex,從而從condition_wait()中返回。 

C代碼   收藏代碼
  1. //...  
  2. condition_signal(cond);  
  3. 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繼續等待。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM