我們都知道如何使用一個condition variable:
1、Linux下:
1 pthread_mutex_lock(&mutex);
2 pthread_cond_wait(&cond, &mutex);
3 doSomething();
4 pthread_mutex_unlock(&mutex);
2、java里:
1 synchronized(this){
2 wait();
3 doSomething();
4 }
3、C#里:
1 monitor.Enter();
2 monitor.Wait();
3 doSomething();
4 monitor.Exit();
4、使用Win32API
EnterCriticalSection (&cs);
SleepConditionVariableCS (&cond, &cs, INFINITE);
doSomething();
LeaveCriticalSection (&cs);
不難看到,不管是哪種語言,不論使用什么程序庫,無論在windows下亦或是Linux下,condition variable這個東西的用法似乎都是固定的:必須和一把鎖搭配使用。
可是,問題是為什么這些庫、框架、系統的設計者,在設計這套機制的時候,非要讓我們好死不死再和一把鎖一塊兒使用呢?做這種費力不討好的事兒,到底是因為歷史原因,還是另有深意呢?
為了回答這個問題,首先考慮這么一個生產者消費者的場景。生產者會生產數據將其放到dataHandler對象中,然后向消費者發送一條消息。消費者也有dataHandler示例的句柄,並且在接收到生產者的消息后,它將通過dataHandler.getData()來獲取數據。假設這個世界上的condition variable全是沒有mutex機制的,則消費者的實現代碼可能是這樣的:
1 if(!bDataReady){
2 pthread_cond_wait(&sigDataReady); //根據假設,我們現在不需要鎖了
3 }
4 data = dataHandler.getData();
5 dataHandler.releaseData();
代碼的邏輯十分簡單。但是,現在我們考慮一下,如果有以下執行順序:
消費者:檢查bDataReady不為真
生產者:設置bDataReady為true
生產者:發送消息
消費者:等待condition variable
這時,雖然生產者通知了資源的可用,但由於這時候消費者尚未開始等待消息,也就沒能接收到這個消息,於是只能在消息消失后無盡地等待下去。另一方面,由於消費者一直的等待,導致資源不被消費,在糟糕的情況下生產者會等待消費者消費完畢。於是乎,死鎖發生了。
思考一下,為什么會發生死鎖呢?其本質的原因在於生產者、消費者的行為沒有保證原子性(Atomic)。這時候,我們要問了,既然pthread_cond_wait是系統級別的同步互斥工具,它怎么會不能保證原子性?當然,我並非指這個意義上的原子性。我們首先要知道condition variable的設計意圖,實際上condition variable描述這么一種情景:在這個情境中,有一個人在等待一個特定事件,而另一個人會在該事件發生時告知前者。但是,condition variable本身只實現這么一種機制,它並不指出到底是什么事件發生了。因此,往往我們在等待成功,知道有事件發生的話,還需要額外代碼來判斷是哪個事件。注意,這就是整個問題的關鍵點:
實際上我們在使用condition variable時,不僅要通condition variable實現通知和被通知,還要自行實現通知和被通知前后所必要的判斷、處理等業務邏輯,這才是condition variable的使用模型。知道這個后,我們就能理解為什么上面的情景會出問題了。原因在於消費者的基本行為沒有確保原子性,它的判斷和等待被分隔了。
我們當然可以爭辯道,即使不通過和mutex合作,我們一樣能使用這個假想的condition variable很好的完成功能。是的,我承認作為程序員你可以足夠聰明和小心,以至於你實現的版本沒有我上面這段代碼的問題。但是,這難道不和你去跟設計mutex的人爭辯說你明明可以自己通過精心設計實現互斥,根本不用mutex橫插一腳這件事一樣愚蠢且毫無意義么?誰也不能阻止你選擇使用自己的辦法,但是問題的關鍵是,我們需要一種有經過理論證明研究過的可靠的、統一的方法,因為軟件開發在大部分情況下是一門工程,而不是一門任你發揮的藝術。