1. 程式人生 > >深入解析條件變量(condition variables)

深入解析條件變量(condition variables)

atom 情況 隊列 中文版 .net pri 獲取 UC 發送

深入解析條件變量

什麽是條件變量(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?

indirect priority inversion

pthread_cond_signal 和 pthread_mutex_unlock順序問題

深入解析條件變量(condition variables)