1. 程式人生 > >【併發】多執行緒程式設計中條件變數和虛假喚醒的討論

【併發】多執行緒程式設計中條件變數和虛假喚醒的討論

轉自:http://blog.csdn.net/puncha/article/details/8493862

From: http://siwind.iteye.com/blog/1469216

From:http://en.wikipedia.org/wiki/Spurious_wakeup

According to David R. Butenhof's Programming with POSIX Threads ISBN 0-201-63392-2:

"

Spurious wakeup

means that when you wait on a condition variable, the wait may (occasionally) return when no thread specifically broadcast or signaled that condition variable. Spurious wakeups may sound strange, but on some multiprocessor systems, making condition wakeup completely predictable might substantially slow all condition variable operations. The 
race conditions
 that cause spurious wakeups should be considered rare."

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

一般的程式設計模式: 
Java程式碼  收藏程式碼
  1. var mutex;  
  2. var cond;  
  3. var something;  
  4. Thread1: (等待執行緒)  
  5. lock(mutex);  
  6. while( something not true ){  
  7.     condition_wait( cond, mutex);  
  8. }  
  9. do(something);  
  10. unlock(mutex);  
  11. ============================  
  12. Thread2: (解鎖執行緒)  
  13. do(something);  
  14. ....  
  15. something = true;  
  16. unlock(mutex);  
  17. condition_signal(cond);  

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

2. 函式介面

Java程式碼  收藏程式碼
  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是兩個單獨的操作,那麼就存在一個順序的問題。誰先隨後可能會產生不同的結果。如下: 
(1) 按照 unlock(mutex); condition_signal()順序, 當等待的執行緒被喚醒時,因為mutex已經解鎖,因此被喚醒的執行緒很容易就鎖住了mutex然後從conditon_wait()中返回了。 
(2) 按照 condition_signal(); unlock(mutext)順序,當等待執行緒被喚醒時,它試圖鎖住mutex,但是如果此時mutex還未解鎖,則執行緒又進入睡眠,mutex成功解鎖後,此執行緒在再次被喚醒並鎖住mutex,從而從condition_wait()中返回。


可以看到,按照(2)的順序,對等待執行緒可能會發生2次的上下文切換,嚴重影響效能。因此在後來的實現中,對(2)的情況,如果執行緒被喚醒但是不能鎖住mutex,則執行緒被轉移(morphing)到互斥量mutex的等待佇列中,避免了上下文的切換造成的開銷。 -- wait morphing

程式設計時,推薦採用(1)的順序解鎖和發喚醒訊號而Java程式設計只能按照(2)的順序,否則發生異常!!。 

在SUSv3http://en.wikipedia.org/wiki/Single_UNIX_Specification的規範中(pthread),指明瞭這兩種順序不管採用哪種,其實現效果都是一樣的。