一直以來都天真的認為線程間同步的方法只有信號量,互斥量,郵箱,消息隊列,知道最近開始研究一些Linux方面的代碼才發現自己是多么的很傻很天真。在Linux中還存在這一種叫做條件變量的東西。必須承認我在理解這個概念上花了很多時間,查閱了很多資料。這里主要分析如下幾個問題:1. 條件變量是什么;2.為什么要和互斥量配合使用,互斥量保護的是什么;3.為什么條件變量經常會和while配合使用。
1. 什么是條件變量
條件變量是線程同步的一種手段。條件變量用來自動阻塞一個線程,直到條件(predicate)滿足被觸發為止。通常情況下條件變量和互斥鎖同時使用。
條件變量使我們可以睡眠等待某種條件出現。條件變量是利用線程間共享的全局變量進行同步的一種機制,主要包括兩個動作:一個/多個線程等待"條件變量的條件成立"而掛起;另一個線程使"條件成立"信號。
2. Mutex的作用
這里讓我們先用一段代碼說明一般條件變量是如何和互斥量配合使用的:
1 int WaitForPredicate() 2 { 3 // lock mutex (means:lock access to the predicate) 4 pthread_mutex_lock(&mtx); 5 6 // we can safely check this, since no one else should be 7 // changing it unless they have the mutex, which they don't 8 // because we just locked it. 9 while (!predicate) 10 { 11 // predicate not met, so begin waiting for notification 12 // it has been changed *and* release access to change it 13 // to anyone wanting to by unlatching the mutex, doing 14 // both (start waiting and unlatching) atomically 15 pthread_cond_wait(&cv,&mtx); 16 } 17 18 // we own the mutex here. further, we have assessed the 19 // predicate is true (thus how we broke the loop). 20 21 // You *must* release the mutex before we leave. 22 pthread_mutex_unlock(&mtx); 23 }
那這個互斥量保護的是什么呢?是條件變量本身么?並不是!mutex使用來保護predicate。mutex被成功lock后我們就可以放心的去讀取predicate的值,而不用擔心在這期間predicate會被其他線程修改。如果predicate不滿足條件,當前線程阻塞等待其他線程釋放條件成立信號,並釋放已經lock的mutex。這樣一來其他線程就有了修改predicate的機會。當其他線程釋放條件成立信號后,pthread_cond_wait函數返回,並再次lock mutex。
pthread_cond_wait的工作流程可以總結為:unlock mutex,start waiting -> lock mutex。
3. while的作用
在上面的代碼當中可以看到,predicate是用while來檢查的而不是用if在做判斷。這樣做的原因是,pthread_cond_wait的返回並不一定意味着其他線程釋放了條件成立信號。而是意外返回。這種情況稱為Spurious wakeup。之所以這樣做的原因是從效率上考慮的。Volodya's blog - Spurious wakeups里有很詳細的一個講解,簡單來說造成Spurious wakeup的原因在於,Linux中帶阻塞功能的system call都會在進程收到了一個signal后返回。這就是為什么要用while來檢查的原因。因為我們並不能保證wait函數返回就一定是條件滿足,如果條件不滿足,還需要繼續等待。
4. Signal條件變量時的考慮
因為在signal線程中解鎖互斥量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次的上下文切換,影響性能. 那使用(1)又是什么情況呢,在unlock之后,其他的線程就可以獲取這個mutex進而修改了predicate,這樣signal就失去了意義,也和我們的最初想法不一樣。就我個人而言,我會選擇使用(2)。