線程同步:條件變量的使用細節分析

如同互斥量和讀寫鎖一樣,條件變量也需要初始化和回收
#include
int pthread_cond_init(pthread_cond_t *restrict cond,
pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);


互斥量和讀寫鎖解決了多線程訪問共享變量產生的競爭問題,那麼條件變量的作用何在呢。


條件變量的作用在於他給多個線程提供了一個匯合的場所。什麼意思呢?
舉個最簡單的例子,比如運動會賽跑中,所有選手都會等到發令槍響後纔會跑,吧選手比作
其他的子線程。發令員比作主線程。 那麼就是說,所有的子線程現在都在等待主線程給予
一個可以運行的信號(發令槍響)。這就是這些子線程的匯合點。如果主線程沒給信號,那麼子線程就會阻塞下去。

大概明白了 條件變量的作用,現在我們來考慮 第一個使用細節上的問題

考慮一個情況:b c d 三個線程都期望在一個條件變量等待主線程發送信號,如果此時條件測試爲假,那麼三個線程下一步應該是阻塞休眠。
但是在判斷條件不正確和休眠這之間有個時間窗口,假如在bcd三個線程檢查條件爲假後,cpu切換到另一個線程A,
在線程A中卻使條件變爲真了。那麼當cpu切換回bcd線程中時線程還是會休眠。也就是說在線程檢查條件變量和進入休眠等待
條件改變這兩個操作之間存在一個時間窗口。這裏存在着競爭。


我們知道互斥量是可以用來解決上面的競爭問題的,所以條件變量本身 是由互斥量來保護的
既然判斷和睡眠是由互斥量來保護從而成爲一個原子操作,那麼其他改變條件的線程就應該以一致的方式修改條件
也就是說其他線程在改變條件狀態前也必須首先鎖住互斥量。(如果修改操作不是用互斥量來保護的話,那麼判斷和休眠使用互斥量來保護也就沒有意義。因爲其他線程還是可以在兩個操作的空隙中改變條件。但是如果修改操作也使用互斥量。因爲判斷和休眠之前先加鎖了。那麼修改操作就只能等到判斷失敗和休眠兩個操作完成才能進行而不會在這之間修改)

下面是提供的接口:
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restricr mutex,
const struct timespec *restrict timeout);

使用pthread_cond_wait等待條件變爲真,傳遞給pthread_cond_wait的互斥量對條件變量進行保護,調用者把鎖住的互斥量傳遞給函數。

(互斥量在傳遞給函數之前已經調用pthread_mutex_lock鎖住) 
函數把調用線程放到等待條件的線程列表上,然後對互斥量解鎖。這樣使得判斷和休眠成了原子操作。也就關閉了他們之間的
時間窗口。

當pthread_cond_wait返回時,會重新獲取互斥量(互斥量再次被鎖住)。
 
 pthread_cond_timedwait與pthread_cond_wait的區別在於它指定了休眠的時間,如果時間到了,但是條件還是沒有出現,那麼pthread_wait_timedwait也將
 重新獲取互斥量。然後返回 錯誤ETIMEDOUT
 需要注意的一點是。pthread_cond_timedwait的參數timeout不是相對值,而是絕對值。比如你想最多休眠三分鐘,那麼timeout不是3分鐘
 而是當前時間加上3分鐘。
 
 有兩個函數可以用來通知線程條件已滿足。pthread_cond_signal函數將喚醒等待該條件的某個線程。
 pthread_cond_broadcast函數將喚醒等待該條件的所有線程。
 
 int pthread_cond_signal(pthread_cond_t *cond);
 int pthread_cond_broadcast(pthread_cond_t *cond);
 
 現在我們來看一個具體的例子。
 
 在下面這個程序中兩個子線程在一個條件變量cond上等待條件 i 等於一億成立。主線程中對 i 做自增操作,當i增加到一億的時候。條件成立 那麼主線程 向條件變量發送信號。那麼兩個子線程就會從休眠中醒來從而繼續運行。
 
 pthread_mutex_t mutex;
pthread_cond_t  cond;
unsigned long i=0;


void *th1(void *arg){
        pthread_mutex_lock(&mutex);            //條件變量是由互斥量來保護的
     
        pthread_cond_wait(&cond,&mutex);
     
        pthread_mutex_unlock(&mutex);
        printf("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
        pthread_exit((void *)0);
}
void *th2(void *arg){
        pthread_mutex_lock(&mutex);
     
        pthread_cond_wait(&cond,&mutex);
     
        pthread_mutex_unlock(&mutex);
        printf("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n");
        pthread_exit((void *)0);
}
int main(void){
        pthread_mutex_init(&mutex,NULL);
        pthread_cond_init(&cond,NULL);


        pthread_t t1,t2;
        pthread_create(&t1,NULL,th1,(void *)0);
        pthread_create(&t2,NULL,th2,(void *)0);


        while(1){       
                pthread_mutex_lock(&mutex);          // i爲兩個子線程等待的條件,就像上面說的修改它也應該先鎖住互斥量
                i++;
                pthread_mutex_unlock(&mutex);
                if(i==100000000){
                        pthread_cond_broadcast(&cond);
                        break;
                }
        }
        pthread_join(t1,NULL);
        pthread_join(t2,NULL);


        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&mutex);
        exit(0);
}
 程序運行後停頓幾秒輸出:
 bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
也就是主線程中不停的 對i 進行自增操作。當i等於一億的時候,那麼條件滿足。主線程向條件變量發送信號。兩個在條件變量殺上等待
的子線程收到信號後便開始運行。


但是 這個程序是存在問題的。這是我們要說的 第二個關於條件變量上的細節。

在兩個子線程中 我們只是簡單的使用了  pthread_cond_wait(&cond,&mutex); 在條件變量上休眠等待主線程發送信號過來。
但是在pthread_cond_wait調用等待的時候,線程是釋放鎖的。(當他返回時纔會再次獲得鎖)。
那麼就存在一個問題

假想一下。當主線程發送信號過來後。在子線程 在pthread_cond_wait上等待發現信號發過來了,那麼子線程將醒來並運行(注意這個時候pthread_cond_wait還未返回,那麼鎖是釋放的,因爲pthread_cond_wait在等待是會釋放鎖,返回時纔會重新獲得鎖),那麼如果這時候另一個線程改變了 i(對i進行了增減操作。)
那麼此時i 不在是 一億。但是切換到子線程時他並不知情,他會仍舊認爲條件是滿足的。也就是說 我們不應該僅僅依靠pthread_cond_wait的返回
就認爲條件滿足。
所以 上面的程序 中 子線程中的 pthread_cond_wait(&cond,&mutex) 應該改爲:
while(i!=100000000){
pthread_cond_wait(&cond,&mutex);
}
這樣即使 在子線程中 pthread_cond_wait返回前還未獲得鎖的這段空隙有其他線程改變了 i 使條件不在成立。那麼當pthread_cond_wait返回時他仍舊能發現 i 條件不成立。就會繼續調用pthread_cond_wait再條件變量上等待。


最後再來看個上面的一個問題:


在給  在條件變量上等待的線程  發送信號的線程中有下面兩個步驟;
a:
(1)對互斥量加鎖(pthread_mutex_lock)
(2)改變互斥量保護的條件。(對應上面的例子就是在主線程中的 i++ 操作)
(3)向等待條件的線程發送信號(pthread_cond_broadcast)
(4)對互斥量解鎖(pthread_mutex_unlock)
b:
(1)對互斥量加鎖(pthread_mutex_lock)
(2)改變互斥量保護的條件。(對應上面的例子就是在主線程中的 i++ 操作)
(3)對互斥量解鎖(pthread_mutex_unlock)
  (4)向等待條件的線程發送信號(pthread_cond_broadcast)
  
這兩種步驟其實都是可以的 但是都存在一些不足。
在 a 步驟中。 也就是主線程在發送條件成立信號在解鎖前。(上面給的例子是在解鎖後,在b中會說明)
那麼也就是主線程發送信號後還是持有鎖的,當子線程收到信號後會結束休眠
但是前面說過pthread_cond_wait返回時會再次獲得鎖,但是主線程還並未釋放
鎖,所以會造成子線程收到信號開始運行並立即阻塞

在b步驟中。  主線程在釋放鎖後才發送信號。我們上面的例子就是這麼做的。但是這也存在一個問題

但釋放鎖後,另一個線程很可能會在發送信號之前獲得鎖並修改 變量i 導致條件再次不成立
但是會到主線程中他卻並不知情,導致仍會發送信號給子線程。子線程認爲條件滿足
從休眠中醒來開始運行,但此時條件是不滿足的。
所以在上面的例子中我們將
pthread_cond_wait(&cond,&mutex) 改爲:
while(i!=100000000){
pthread_cond_wait(&cond,&mutex);
}
讓子線程醒來後再次判斷條件是否成立。這樣就可以避免了上面的問題。


總結一下: 條件變量的要點在於 他提供了一個讓多個線程匯合的點。但是條件變量本身是需要
互斥量來進行保護的。
我們不能僅僅根據pthread_cond_wait返回就認爲條件滿足了。而需再次判斷條件是否正確
發佈了23 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章