深入解析條件變量#
什么是條件變量(condition variables)##
引用APUE中的一句話:
Condition variables are another synchronization mechanism available to threads.
These synchronization objects provide a place for threads to rendezvous. When used with mutexes, condition variables allow threads to wait in a race-free way for arbitrary conditions to occur.
條件變量是線程的另外一種同步機制,這些同步對象為線程提供了會合的場所,理解起來就是兩個(或者多個)線程需要碰頭(或者說進行交互-一個線程給另外的一個或者多個線程發送消息),我們指定在條件變量這個地方發生,一個線程用於修改這個變量使其滿足其它線程繼續往下執行的條件,其它線程則接收條件已經發生改變的信號。
條件變量同鎖一起使用使得線程可以以一種無競爭的方式等待任意條件的發生。所謂無競爭就是,條件改變這個信號會發送到所有等待這個信號的線程。而不是說一個線程接受到這個消息而其它線程就接收不到了。
一個例子##
具體的函數介紹就不說了,詳細參考APUE,下面通過一個例子來詳細說一下正確使用條件變量的方法。下例實現了生產者和消費者模型,生產者向隊列中插入數據,消費者則在生產者發出隊列准備好(有數據了)后接收消息,然后取出數據進行處理。實現的關鍵點在以下幾個方面:
- 生產者和消費者都對條件變量的使用加了鎖
- 消費者調用pthread_cond_wait,等待隊列是否准備好的信息,注意參數有兩個,一個是pthread_cond_t,另外一個是pthread_mutex_t.
代碼:
#include <pthread.h>
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void
process_msg(void)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
關於上面例子的幾個疑問##
為什么pthread_cond_wait需要加鎖??###
pthread_cond_wait中的mutex用於保護條件變量,調用這個函數進行等待條件的發生時,mutex會被自動釋放,以供其它線程(生產者)改變條件,pthread_cond_wait中的兩個步驟必須是原子性的(atomically,萬惡的APUE中文版把這個單詞翻譯成了『自動』,誤人子弟啊),也就是說必須把兩個步驟捆綁到一起:
- 把調用線程放到條件等待隊列上
- 釋放mutex
不然呢,如果不是原子性的,上面的兩個步驟中間就可能插入其它操作。比如,如果先釋放mutex,這時候生產者線程向隊列中添加數據,然后signal,之后消費者線程才去『把調用線程放到等待隊列上』,signal信號就這樣被丟失了。
如果先把調用線程放到條件等待隊列上,這時候另外一個線程發送了pthread_cond_signal(我們知道這個函數的調用是不需要mutex的),然后調用線程立即獲取mutex,兩次獲取mutex會產生deadlock.
在生產者線程中修改條件時為什么要加mutex??###
如果不這么做信號可能會丟失,看下面的例子:
Thead A Thread B
pthread_mutex_lock(&qlock);
while (workq == NULL)
mp->m_next = workq;
workq = mp;
pthread_cond_signal(&cond);
pthread_cond_wait(&qready, &qlock);
在while判斷之后向隊列中插入數據,雖然已經有數據了,但線程A還是調用了pthread_cond_wait等待下一個信號到來。。
消費者線程中判斷條件為什么要放在while中??###
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
我們把while換成if可不可以呢?
if (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
答案是不可以,一個生產者可能對應着多個消費者,生產者向隊列中插入一條數據之后發出signal,然后各個消費者線程的pthread_cond_wait獲取mutex后返回,當然,這里只有一個線程獲取到了mutex,然后進行處理,其它線程會pending在這里,處理線程處理完畢之后釋放mutex,剛才等待的線程中有一個獲取mutex,如果這里用if,就會在當前隊列為空的狀態下繼續往下處理,這顯然是不合理的。
signal到底是放在unlock之前還是之后??###
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
如果先unlock,再signal,如果這時候有一個消費者線程恰好獲取mutex,然后進入條件判斷,這里就會判斷成功,從而跳過pthread_cond_wait,下面的signal就會不起作用;另外一種情況,一個優先級更低的不需要條件判斷的線程正好也需要這個mutex,這時候就會轉去執行這個優先級低的線程,就違背了設計的初衷。
void
enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_cond_signal(&qready);
pthread_mutex_unlock(&qlock);
}
如果把signal放在unlock之前,消費者線程會被喚醒,獲取mutex發現獲取不到,就又去sleep了。浪費了資源.但是在LinuxThreads或者NPTL里面,就不會有這個問題,因為在Linux 線程中,有兩個隊列,分別是cond_wait隊列和mutex_lock隊列, cond_signal只是讓線程從cond_wait隊列移到mutex_lock隊列,而不用返回到用戶空間,不會有性能的損耗。
所以在Linux中推薦使用這種模式。
References:
why pthread_cond_wait need an lock?
Calling pthread_cond_signal without locking mutex
Why do pthreads’ condition variable functions require a mutex?