pthread_cond_wait 為什麼需要傳遞 mutex 參數?

網上找到的解釋:「無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的競爭條件(Race Condition)。」

但是,我閱讀了源碼(pthread_cond_wait.c)之後覺得並不是上述原因。因為,傳遞的互斥鎖並未用於函數內部的同步,函數內部的同步機制完全是依賴於struct condition_variable中的信號量和互斥鎖實現的,而非mtxExternal(傳遞的函數參數)。

也就是說,對於同一個條件變數,用戶可以從多個線程、傳遞不同的mutex同時調用pthread_cond_wait()函數。那麼,為什麼該函數要求用戶必須獲得mutex呢?


——————更新,勘誤——————

昨天晚上回到宿舍谷歌到了官方文檔(在機房的時候谷歌掛了,怎麼搜都搜不到……),發現之前的解釋有個錯誤,即 @高大月 同學指出的「原子」語義。我結合昨天寫的測試代碼再補充一下。

我在原來的答案中,有這樣的代碼:

pthread_mutex_unlock(mtx);
pthread_cond_just_wait(cv);
pthread_mutex_lock(mtx);

事實上,上面三行代碼的並不是pthread_cond_wait(cv, mtx)的內聯展開。其中第一行和第二行必須「原子化」,而第三行是可以分離出去的(之所以要把第三行放在裡面的原因可以參見原來的答案)。

那麼為什麼第一行和第二行不能分離呢?這是因為必須得保證:如果線程A先進入wait函數(即使沒有進入實際的等待狀態,比如正在釋放mtx),那麼必須得保證其他線程在其之後調用的broadcast必須能夠將線程A喚醒。

所以,把原來答案中的代碼再貼一遍:

// 線程A,條件測試
pthread_mutex_lock(mtx); // a1
while(pass == 0) { // a2
pthread_mutex_unlock(mtx); // a3
pthread_cond_just_wait(cv); // a4
pthread_mutex_lock(mtx); // a5
}
pthread_mutex_unlock(mtx);

// 線程B,條件發生修改,對應的signal代碼
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4

如果執行序列是:a1, a2, a3, b1, b2, b3, b4, a4,那麼線程A將不會被喚醒。而a3在線程B之前執行,這意味著wait函數是在signal之前調用的,所以不滿足上文提到的保證。

解決辦法:

  1. 先將線程附加到等待隊列
  2. 釋放mutex
  3. 進入等待

感興趣的同學的可以看下源碼(pthread_cond_wait.c),附加到等待隊列這個操作是加鎖的,所以可以保證之前發起的signal不會錯誤得喚醒本線程,而之後發起的signal必然喚醒本線程。

因此,下面的代碼是絕對不會出錯的:

// 線程A,條件測試
pthread_mutex_lock(mtx); // a1
while(pass == 0) { // a2
pthread_cond_wait(cv, mtx); // a3
}
pthread_mutex_unlock(mtx); // a4

// 線程B,條件發生修改,對應的signal代碼
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4

如果線程A先運行,那麼執行序列必然是:a1, a2, a3, b1, b2, b3, b4, a4。

如果線程B先運行,那麼執行序列可能是:b1, b2, b3, b4, a1, a2, a4

也可能是:b1, b2, b3, a1, a2, a3, b4, a4

所以,如果是我設計pthread API,那麼我會添加一個pthread_cond_unlock_and_wait函數,偽代碼如下:

int pthread_cond_wait(cv, mtx) {
int ret = pthread_cond_unlock_and_wait(cv, mtx);
pthread_mutex_lock(mtx);
return ret;
}

// 線程A,條件測試
pthread_mutex_lock(mtx);
if (pass == 0)
pthread_cond_unlock_and_wait(cv, mtx);
else
pthread_mutex_unlock(mtx);

// 線程B,條件發生修改,對應的signal代碼
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4

這樣的好處在於:如果我們可以保證沒有虛假喚醒(即不需要while循環測試條件),那麼我們可以將線程A的代碼改成上述形式,這樣無論怎樣都只需要執行一次pthread_mutex_unlock()函數,而之前的版本至少需要執行兩次。

—————— 原來的答案——————

感謝大家的回答!

看了之後,我獲得了啟發,突然覺得這或許是跟條件變數的通常用法有關。

首先需要明白兩點:

  • wait()操作通常伴隨著條件檢測,如:

    while(pass == 0)
    pthread_cond_wait(...);

  • signal*()函數通常伴隨著條件改變,如:

    pass = 1;
    pthread_cond_signal(...)

由於此兩處都涉及到變數pass,所以為了防止Race Condition,必須得加鎖。所以代碼會變成下面這樣:

// 條件測試
pthread_mutex_lock(mtx);
while(pass == 0)
pthread_cond_wait(...);
pthread_mutex_unlock(mtx);

// 條件發生修改,對應的signal代碼
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(...);

然後,我們假設wait()操作不會自動釋放、獲取鎖,那麼代碼會變成這樣:

// 條件測試
pthread_mutex_lock(mtx);
while(pass == 0) {
pthread_mutex_unlock(mtx);
pthread_cond_just_wait(cv);
pthread_mutex_lock(mtx);
}
pthread_mutex_unlock(mtx);

// 條件發生修改,對應的signal代碼
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(cv);

久而久之,程序員發現unlock, just_wait, lock這三個操作始終得在一起。於是就提供了一個pthread_cond_wait()函數來同時完成這三個函數。

另外一個證據是,signal()函數是不需要傳遞mutex參數的,所以關於mutex參數是用於同步wait()和signal()函數的說法更加站不住腳。

所以我的結論是:傳遞的mutex並不是為了防止wait()函數內部的Race Condition!而是因為調用wait()之前你總是獲得了某個mutex(例如用於解決此處pass變數的Race Condition的mutex),並且這個mutex在你調用wait()之前必須得釋放掉,調用wait()之後必須得重新獲取。

所以,pthread_cond_wait()函數不是一個細粒度的函數,卻是一個實用的函數。


Condition Variables (Windows)

雖然不是那個函數,但是這個API做了跟pthread_cond_wait一樣的事情,你可以通過閱讀remark部分來了解為什麼要這麼干。


條件變數被用於生產者和消費者之間投遞通知用的。不過還是要舉例說明。

假如說有個臨界資源,需要AB2個線程以ABABABABAB.....的順序來訪問。而且不打算靠第3個線程來調度和幫助,就靠他們2個互相同步。

用一個鎖?還真未必安全,A解鎖之後再嘗試加鎖的過程中,B未必就能加鎖成功。這就可能導致AAB的順序來訪問了。

用2個鎖?初始化A先持有a,b2個鎖,B依次嘗試競爭這2個鎖。A先訪問臨界,再釋放鎖a,再釋放b, 再競爭a,再競爭b; B先競爭a, 再競爭b,再訪問臨界。。現在鎖實際變大了,依然有可能A把2個鎖都釋放了再競爭2個鎖成功,B依然沒加鎖成功一個鎖。 此外讓AB獲取鎖順序不一致的任何方法都可能導致死鎖。

仔細一想:我們需要的是A,B在每次通知的過程中「原子交換」彼此手中的鎖才能實現上述同步目標。我們把其中一個鎖標記為「條件變數」,持有(或者未持有)條件變數的線程才能訪問臨界。每次訪問完之後,再把條件變數(或另一個鎖)嘗試交換出去。這樣就可以實現交錯串列化訪問臨界資源了。 不過還是有很多初始化啊之類的問題等著解決。

pthread庫選擇的語義是:默認線程都持有條件變數(鎖,從不參與lock函數),競爭臨界資源的時候總在嘗試交換(lock)到另外一個鎖,這樣使得文法變得很自然,讓大家覺得自己在unlock mutex前可以訪問臨界資源。是的,原子交換並不需要對交換的2個操作數都支持加鎖/解鎖操作。

(這裡我說條件變數是假鎖的含義是:如果你要用「原子交換」語義去理解條件變數的話,你需要隱式的認為某些語句在獲取或者釋放條件變數,哪怕那行語句中根本沒有出現條件變數!!

事實上另外一個鎖如果被多個線程競爭,我們輕鬆的就可以實現為一對多的交錯串列化訪問臨界資源(A, B或C或D,A, B或C或D,A, B或C或D,........ BCD競爭那個鎖),或者叫一對多的生產者消費者關係。pthread_cond_signal 和pthread_cond_wait 此時就表現出這種不對等性了。

上述方案順手實現了上面那些答案所提到的初始化衝突等問題。下面用實際代碼來說明。

定義:

0. 你可以在任何初始化的場景中認為你已經獲得了假鎖。

1. 用真鎖交換假鎖總是會當場成功的。而用假鎖交換真鎖則可能會等待。

流程:

生產者生成完畢並準備發布時總是持有真鎖並交換之。

pthread_mutex_lock(mtx); //用假鎖嘗試交換真鎖

//生產blababalblal

pthread_cond_signal(cond);

pthread_mutex_unlock(mtx); //上面2句合一起表示用真鎖交換給某一個假鎖。

而消費者們總是先獲得真鎖 再交換得了假鎖,進而嘗試用假鎖交換真鎖。2次嘗試獲得真鎖的過程可以保證,如果他比生產者先被創建而先獲得真鎖,他也會陷入等待交換的過程中而不會去把生產者給鎖了或者忙等生產者。

pthread_mutex_lock(mtx); //用假鎖嘗試交換真鎖

pthread_cond_wait(cond, mtx); //用真鎖交換假鎖,再用假鎖嘗試交換真鎖

//可能生產者在你認為未生產好就把真鎖換給你,也可能存在多個消費者競態而被驚群喚起的情況(這其實是一種病態行為,不能用上面的語義去解釋了),需要用while+檢查條件把上面1句包起來檢查

//確認你拿到真鎖並且符合消費條件就開始消費吧。。。。

pthread_mutex_unlock(mtx); //現在用真鎖交換假鎖

注意pthread實現生產者可以直接pthread_cond_signal任何消費者而無需配合mutex,此時的語義則表示為:生產者想提醒某消費者(我不持有鎖,無需和你配合做某事,但是我想單向通知你)可以消費啦。此時不再是原有的生產一個,消費一個,再生產一個的串列模型了,而是非同步消費的過程了(我只管生產,你或你們消費者隨便消費)。不過這時的模型用單鎖其實就足夠了,,類似nginx多進程模型。

------------------------------------------------------------------------------------------------------------------------

總之,如果只用一個"條件變數"就能幹的事情,鎖一定都可以干(同步設施如果數量為1,那麼語義必定相同,即使api之類的形式不同)。


有幾篇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);
}
};

論文中也有條件變數(conditional variable),使用形式是cond_var.wait()和cond_var.signal()。讓我們來看一下論文里最簡單的一個例子。這是同步對單個資源訪問的monitor。原文中的代碼(好像)是用Pascal寫的,我這裡有C-style重寫了一下。

//注意這是一個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
};

需要特別注意,這是一個monitor(不是class)。其中的acquire()和release()不能也不會同時active。我們注意到,這裡的nonbusy.wait()並沒有使用lock作為參數。但是,Hoare其實是假設有的。只是在論文中,他把這個lock叫做monitor invariant。論文中,Hoare解釋了為什麼要在conditional wait時用到這個值(也就解釋了樓主的問題)。我原文引用一下:

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]).

有興趣的同學可以讀讀這篇文章,文中有一節專門解釋了樓主的問題。樓主的問題顯然是很深刻的。

另外,為什麼上面的代碼里acquire()的實現使用的是:

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的值。如果還不是想要的,就會再次等待。

之前回答中,吳志強同學的這段代碼是有問題的:

int pthread_cond_wait(cv, mtx) {
int ret = pthread_cond_unlock_and_wait(cv, mtx);
pthread_mutex_lock(mtx);
return ret;
}

// 線程A,條件測試
pthread_mutex_lock(mtx);
if (pass == 0)
pthread_cond_unlock_and_wait(cv, mtx);
else
pthread_mutex_unlock(mtx);

// 線程B,條件發生修改,對應的signal代碼
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4

首先,我沒有理解為什麼 pthread_cond_wait()被定義了但沒有被調用。也許應該是這樣吧:

if (pass == 0)
pthread_cond_wait(cv, mtx);

但是這樣之後有兩個問題。首先,線程A從等待恢復之後,沒有unlock。其次,這裡只使用了if,會產生我之前提到的問題。

......... Update 06/15/2014 .........

我覺得這是一個很好的例子,讓我來試著討論一下這段代碼。

// code 1 // line number
pthread_mutex_lock(mtx); // a1
if (pass == 0) // a2
pthread_cond_unlock_and_wait(cv, mtx); // a3
else // a4
pthread_mutex_unlock(mtx); // a5
// mark // a6

// code2 // line number
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4

首先,假設有「虛假喚醒(也就是說只要被喚醒,那麼條件必然滿足)」,這段代碼也漏掉了一行重要的代碼,就是

pass = 0

這相當於Hoare的論文中的

busy = true

這如果沒有這行代碼,這段代碼的完整性就有待商討。因為當code2第一次被執行後,所有執行code1的線程都不會等待(執行序列不會到達a3,他們可以在同一時間全部在a6),不能達到同步線程的作用。

另外,要在這個新的設計上加上pass = 0也很困難。試看下面的幾個例子:

例子1:

// code 1 // line number
pthread_mutex_lock(mtx); // a1
if (pass == 0) // a2
pthread_cond_unlock_and_wait(cv, mtx); // a3
else // a4
pthread_mutex_unlock(mtx); // a5
pass = 0 // a6

這個例子中的代碼還是沒有達到同步的作用,因為如果有10的線程,這十個線程還是有可能同時到a6之間。另外,a6沒有得到mutex的保護,有race condition。

例子2:

// code 1 // line number
pthread_mutex_lock(mtx); // a1
if (pass == 0) // a2
pthread_cond_unlock_and_wait(cv, mtx); // a3
pass = 0 // a3"
else // a4
pass = 0 // a4"
pthread_mutex_unlock(mtx); // a5

因為在這個設計里pthread_cond_unlock_and_wait()並不會在恢復執行時請求mtx(如果會,這段代碼也是有問題的,會有死鎖),所以,a3"沒有得到mutex的保護,有race condition。

這段代碼可以被改正,但是會跟原來設計十分相似。

我再在這裡複製一下這段代碼,討論另一個問題。

// code 1 // line number
pthread_mutex_lock(mtx); // a1
if (pass == 0) // a2
pthread_cond_unlock_and_wait(cv, mtx); // a3
else // a4
pass = 0 // a4"
pthread_mutex_unlock(mtx); // a5

// code2 // line number
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4

這段代碼很難做到沒有「虛假喚醒(也就是說只要被喚醒,那麼條件必然滿足)」。試想下面的執行:

TIME THREAD1 THREAD2 THREAD3 COMMENT
t0 a1 initially, pass == 0
t1 a2
t2 a3 wait for cv
t3 b1
t4 b2
t5 b3 context switch to thread 3
t6 a1
t7 a2 pass=1
t8 a4" 假設這裡正確地將pass置為0
t9 a5
t10 (a3)

在t10時間,thread1看到的狀態是pass==0,而不是它所期待的pass==1。這個情況會發生的根本原因為沒有辦法保證{b3;b4}的原子性。在這個情況下假設沒有」虛假喚醒「有些牽強。

要讓這段代碼沒有「虛假喚醒「,需要把b4放到b3前面(並且要控制好操作系統的調度)。

// code2 (revision)
pthread_mutex_lock(mtx);
pass = 1;
pthread_cond_signal(cv);
pthread_mutex_unlock(mtx);

這樣,有了mutex的保護,加上」等待的線程會立即執行「的假設,上面的代碼才相對正確一些。


基本同意@吳志強 的答案,我再補充一下。

pthread_cond_wait並不只是自動釋放鎖、等待、被wakeup後自動獲取鎖的helper function,因為釋放鎖+等待這兩步需要是「原子」的。

注意我給「原子」加了引號,因為這裡原子的語義並不是真正的原子操作。事實上,這兩步根本沒法實現成原子操作。設想如果pthread_cond_wait這樣實現:

LOCK_ACQUIRE(mylock);
pthread_mutex_unlock(mutex); // 釋放用戶傳進來的mutex
wait(); // 把當前線程放到該CV的等待隊列上
LOCK_RELEASE(mylock);

那麼該線程拿著mylock等待了,誰來釋放呢?

因此,這裡「原子」的意思是,如果線程A調用pthread_cond_wait(cond, mutex)剛釋放完mutex,線程B就獲得了mutex並且signal或者broadcast了cond,那麼需要保證最終的行為跟線程B在線程A wait以後才獲得mutex的行為一致,即沒有丟失signal,線程A不會一直block。

當然,使用條件變數的時候直接認為pthread_cond_wait是原子操作就行了:)

覺得我沒有解釋清楚可以看這裡:

The pthread_cond_wait() and pthread_cond_timedwait() functions are used to block on a condition variable. They are called with mutex locked by the calling thread or undefined behaviour will result.

These functions atomically release mutex and cause the calling thread to block on the condition variable cond; atomically here means "atomically with respect to access by another thread to the mutex and then the condition variable". That is, if another thread is able to acquire the mutex after the about-to-block thread has released it, then a subsequent call to pthread_cond_signal() orpthread_cond_broadcast() in that thread behaves as if it were issued after the about-to-block thread has blocked.


如你從網上找到的解釋所解釋的:

無論哪種等待方式,都必須和一個互斥鎖配合,以防止多個線程同時請求pthread_cond_wait()(或pthread_cond_timedwait(),下同)的競爭條件(Race Condition)。

所以這個mutex是為了不讓多個線程同時請求pthread_cond_wait,因為一但有多個線程同時操作這個Conditional Variable,就不可避免會出現Race Condition。

樓主你在想,如果對於同一個變數,用戶可以從多個線程、傳遞不同的mutex同時調用pthread_cond_wait。這種情況就屬於不讀文檔亂來,無法解決。出了事情就讓他RTFM

至於這個Mutex的作用,可以看這個圖(引自pthread條件變數condition(配合mutex鎖使用),經典,有圖_OpenWrt_新浪博客)

文字解釋就是:這個Mutex就是要保證這個線程在完成等待在這個Conditional Variable上這個動作之前,不能讓別的線程過來發Signal或者是修改Wait List(喚醒或等待)。

樓主可以理解這裡面的Race Condition了么?


如果不用mutex,condition的使用會是這樣:

while (predicates do not hold) {
/* 1 */
pthread_cond_wait(cond);
}

若在1這個點正好條件滿足且signal了一下,那麼這個signal就丟了,上面的代碼可能會陷入永久的等待。mutex就是確保1這個位置不可能穿插signal代碼。當然相應地,signal一端也要加鎖,否則仍然無法確保這點,這也是一個常見錯誤,因為pthread_cond_signal並不要求mutex。

一種特殊情況是,當你的條件簡單到只有一條原子指令時,就可以直接使用futex了。事實上condition主要是簡化多線程用戶代碼的開發,當需要編寫較為底層且性能關鍵的代碼時,你需要深入地了解atomic和Memory barrier。


你說那麼多只會頭暈,我這裡有個從API角度的答案:這兩句話。

pthread_mutex_lock: 獲得鎖則鎖定然後執行第二句話,否則阻塞等鎖。

pthread_cond_wait :1.首先解鎖相當於pthread_mutex_unlock。2.然後建立鎖與條件變數的聯繫,3.等待喚醒,4.喚醒後第一件事情是上鎖相當於pthread_mutex_lock,然後執行下一句話。


這個問題其實並不複雜

上面有的長篇大論,洋洋洒洒說了不少,引經據典追本溯源,但似乎有點沒有真正切入到重點。

這裡詳細著重就樓主的問題回答下:

最核心的答案是:

避免分別調用wait()和signal()的兩個線程產生競爭條件(race condition),從而引起死鎖。

這個死鎖的原因和內核對這兩個方法的實現相關,但簡單來說,是因為這兩個調用的實現並不是原子的,而非原子性是產生競爭的必須條件。

這裡給出一個代碼片段來說明產生競態條件的場景,假設wait和signal操作不需要鎖:

int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
void thr_exit() {
done = 1
Pthread_cond_signal(c);
}

void thr_join() {
if (done == 0)
Pthread_cond_wait(c);
}

void* child(void* arg) {
printf("child
");
thr_exit();
return NULL;
}
int main(int argc, char* argv[]) {
printf("parent: begin
");
pthread_t p;
pthread_create(p, NULL, child, NULL);

thr_join();
printf("parent: end
");

return 0;
}

一個父線程建立一個子線程,並掛起自己等待子線程退出後喚醒。典型的wait()和signal()操作。

敲黑板,看這裡:我們試想一下,假設沒有m這個互斥鎖,在父線程調用wait之後,用戶態程序跳轉內核態開始執行wait的具體代碼,但當wait還未完全完成(因為整個wait操作不是原子的,內核需要將該線程加入等待隊列,這涉及到多個步驟,內核也未將該操作原子化),內核還沒有把當前父線程加入掛起等待隊列時,突然調度器進行了上下文切換,切到子線程,子線程在thr_exit()方法里執行了signal操作,修改標誌變數done,並在隊列里試圖喚醒掛起在該CV(conditional variable)上的其他線程(然而父線程還未加入掛起隊列,因為wait操作執行到一半)。然後子線程wait返回,退出。

接著調度器又將父線程切入,繼續wait調用,把自己加入掛起隊列。結果是父線程永遠被掛起,後續程序被死鎖。

最主要的原因就是這樣。

而正確的代碼應該是這樣:

void thr_exit() {
pthread_mutex_lock(m);
done = 1;
pthread_cond_signal(c);
pthread_mutex_unlock(m);
}

void thr_join() {
pthread_mutex_lock(m);
while (done == 0)
Pthread_cond_wait(c, m);
pthread_mutex_unlock(m);
}

1.通過while確保父線程在下一步之前達到了想要的狀態

2.通過互斥鎖保證自己進入等待隊列時的原子性,避免race condition.

3.父線程在進入wait_cond後會自動解鎖m,從而使子線程在signal的時候可以保證在父線程wait之後才做

4.父線程在從wait被喚醒後,自動會重新獲得m鎖,所以需要再釋放一次

後面有時間的話會把wait和signal兩個調用的源碼拿出來在這裡分享下。

再就是強調一下這裡while的重要性,在mesa語義的實現中總是需要wait返回時重新確認掛起條件是否滿足。

參考文獻:

http://linux.die.net/man/3/pthread_cond_signal

http://pages.cs.wisc.edu/~remzi/OSTEP/threads-cv.pdf

上面某NIO大牛引用的Hoare和Dij的幾篇論文都是並發程序的經典論文,推薦一讀。但對樓主的問題,讀論文用處不大,還是仔細看看Linux手冊比較有用。z


深刻體驗

面向github編程一回事

搞懂代碼原理一回事

特么 搞懂不能決定寫出來

此刻很想有一顆頭腦清晰邏輯思維敏捷的腦子

——————原回答—————

一開始以為懂了,不就一個鎖和條件變數嘛...寫交警叔叔的程序的時候發現凌亂了,並沒有真正理解cond和mutex之間的關係o(TヘTo)所以哼哧哼哧了很久,剛好把前面的答案都看了一下,然而我蠢,並沒太懂,所以寫個答案整理一下,如果有bug跪求大佬們留言指出。

正經臉.jpg開始

1. 首先是Oracle官方手冊上關於3個函數的解釋:

1)int pthread_mutex_lock(pthread_mutex_t *mutex); 鎖定互斥鎖:當該函數返回時,該互斥鎖已被鎖定。調用線程是該互斥鎖的屬主。如果該互斥鎖已被另一個線程鎖定和擁有,則調用線程將阻塞,直到該互斥鎖變為可用為止。簡單來說,互斥鎖實現了線程的互斥。

2)int pthread_mutex_unlock(pthread_mutex_t *mutex); 接觸鎖定互斥鎖:解除該互斥鎖當前屬主,允許其他線程對其鎖定和擁有。

3)int pthread_cond_wait(pthread_cond_t *cv, pthread_mutex_t *mutex); 基於條件變數阻塞:這個函數比較神奇(難以真正理解...)該條件獲得信號之前,函數一直被阻塞。

// pthread_cond_wait() 的常用 while() 循環
pthread_mutex_lock();
while(condition_is_false)
pthread_cond_wait();
... //個人覺得這中間可以做點別的事情再unlock
...
...
pthread_mutex_unlock();

一開始沒明白為什麼需要一個while,現在的理解是這樣子的:首先需要一個互斥鎖進行保護,這個原因在第二部分再進行說明。然後對條件表達式進行判斷,如果是假的,那麼線程就會進入while語句,發生條件變數阻塞,同時已經unlock了mutex使其他線程可以使用。採用pthread_cond_wait()之後的主要作用是避免了忙等,也就是說線程不需要一直進行while循環來判斷是否該線程應該運行,而是等到條件變數在其他進程中通過pthread_cond_signal()或者pthread_cond_broadcast()喚醒,導致所有等待該條件的線程解除阻塞並嘗試再次獲取互斥鎖。但是需要注意的是,必須重新測試導致等待的條件,也就是說再次判斷condition_is_false,然後如果符合的。官方手冊上說明while的使用原因是,喚醒的線程重新獲取互斥鎖並在函數返回之前,條件表達式condition_is_false仍然可能發生變化,因此需要使用while語句。

lock mutex-&>condition_is_false-&>unlock mutex-&>wait-&>return-&>lock-&>do sth-&>unlock

總結一下:首先上個互斥鎖進行保護,不滿足運行條件就進行wait自我阻塞,此時函數自動完成unlock使其他線程可以獲得該鎖,等其他線程喚醒該線程之後,停止阻塞獲得該鎖,但是需要while語句再判斷一下條件是否滿足,如果滿足的話就跳出了while循環,該做啥做啥,做完之後unlock一下,其他線程又可以用了。(特么搞懂這個花了快3個小時邊擼官方手冊邊google一下邊寫了一個半小時...嚴重懷疑我是不是該轉專業o(TヘTo) 但是好像終於有點搞明白這個玩意兒了,開心!!!有不對的地方求指出 )

2. 現在搞清楚了mutex和cond之後,才看看為什麼cond一定需要有mutex才可以使用。將使用Oracle的官方手冊上的示例來進行說明(純理論我太蠢,理解不了,直接上代碼說事吧o(TヘTo)

// 示例4-8
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;

decrement_count()
{
pthread_mutex_lock(count_lock);
while(count == 0)
pthread_cond_wait(count_nonzero, count_lock);
count = count - 1;
pthread_mutex_unlock(count_lock);
}

increament_count()
{
pthread_mutex_lock(count_lock);
if(count == 0)
pthread_cond_signal(count_nonzero);
count = count + 1;
pthread_mutex_unlock(count_lock);
}

/* 好的,先去趕個校車...滾回寢室繼續理解為啥要mutex(*/

/* 嗯噠嗯~ o(* ̄▽ ̄*)o 終於滾回來了(我才不會說我水了爪機,給體重稱換了新電池擦的噌噌亮,然後發現體重達到新巔峰...o(TヘTo) 好的好的,立個flag,睡前寫完為什麼pthread_cond_wait需要傳遞參數,這樣明天就可以把大程寫完了吧(●"?"●) */

1)現在先假設沒有mutex存在的情況

// 示例4-8
pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;
unsigned count;

decrement_count()
{
// pthread_mutex_lock(count_lock);
while(count == 0)
pthread_cond_wait(count_nonzero, count_lock);
count = count - 1;
// pthread_mutex_unlock(count_lock);
}

increament_count()
{
// pthread_mutex_lock(count_lock);
if(count == 0)
pthread_cond_signal(count_nonzero);
count = count + 1;
// pthread_mutex_unlock(count_lock);
}

假設我們先運行了decrement_count()函數,並且此時count==0,那麼decrement_count()函數會進行阻塞,直到條件變數count_nonzero被調用,然後函數被喚醒;現在同時increament_count()函數在運行,且count==0,那麼pthread_cond_signal(count_nonzero)會被調用,也就是說decrement_count()被喚醒了,於是 嘀嘀嘀 可怕 事情發生了,兩個線程同時開始了。

原本我們意淫的接下來的狀況應該是這樣的:這個count最開始等於0的,然後在increment_count()函數裡面count++變為1,decreament_count()因為已經被喚醒了,並且再檢測條件變數的時候不等於0了,就跳出了循環,執行了count--的操作。一切,看起來,非常完美。

但是!這只是我們在意淫而已...emmmm....實際的情況是,在increament_count()未執行count++時我們已經用pthread_cond_wait()喚醒了decreament_count(),也就是說進行了線程切換,於是decreament_count()while循環一判斷,特么還是0呀,那你再給我等著吧...結果decreament_count()就一直阻塞在了pthread_cond_wait上了,再也不可能被喚醒了,雖然increament_count()還是會被切回來完成count++。

2)有互斥鎖的情況

代碼就不再copy了。總結來說就是:decreament_count()被pthread_cond_signal喚醒之後一看。第一部分已經提過,pthread_cond_wait()實際上是先unlock了,然後如果signal被喚醒了,在返回前需要lock這個mutex的,也就是說得獲得到互斥鎖才表示成功喚醒了,再去驗證一下while的條件對不對。但是現在wait()發現,丫的我還沒獲取到mutex,increament_count()還佔著呢,於是decreament_count()就只能乖乖等increament_count()跑完count++,把mutex給unlock之後,decreament_count()才能獲得mutex,然後就可以完成我們的意淫啦~

總結來說:mutex函數是用來保證在多線程執行過程中,防止線程由於signal/broadcast而在線程運行過程中發生切換,造成死鎖等bug,保證操作原子性的存在。

(emmmm 老實說,正確性有待考證,都是看著Oracle官方手冊和不理解就Google出來的結果,所以有bug歡迎指出~~~如果有幫助的話,歡迎點贊哈哈哈哈哈哈 感覺自己搞懂了真是心情無比好,開始迎接4天小長假~~(●"?"●)


在最新的glibc中cond_wait已經是用自己的輕量級鎖來保證wait以及signal操作的原子性,避免wait和signal之間的race condation。至於race condation可以很簡單的列出一個。

我們可以考慮一下pthread_cond_wait的實現。

1.檢查signal,如果signal存在,返回,如果不存在轉2

2.把當前線程掛在等待隊列上,做調度。

假設在線程A進入pthread_cond_wait以後,完成了第一步,這個時候signal不存在。條件不滿足。轉向2。

然後線程B調用了pthread_cond_signal。

然後線程A完成了第2步。然後它將永遠也等不到signal了。

用戶態的mutex已經儘早的被釋放掉了。不過釋放的條件是先拿到自己的鎖。為什麼這麼做,我暫時沒想到什麼競爭條件,不過有一個mutex用來保護cond也是應該的,畢竟cond身上的mutex保護不了它自己。


我理解是條件變數也是變數,兩個線程都在使用,那也是要加鎖的


使用條件變數時為啥一定要指定一個鎖?


推薦閱讀:

在哪裡可以找到C語言標準庫的實現源代碼?
為什麼知乎用戶vczh不建議初學編程的人把C作為入門語言?
C語言的取余運算 a%b,如果a<b,那取余a,2%3=2,25%26=25,這是為什麼,規定?
開源代碼里某個函數很長,這種代碼能否認為是好的?
64 位系統中 long double 的最大值是多少?

TAG:編程 | Linux | C編程語言 | 多線程 |