1. 程式人生 > 實用技巧 >【C++多執行緒】條件變數condition_variable

【C++多執行緒】條件變數condition_variable

面向的問題

  當一個執行緒等待另一個執行緒完成任務時,它會有很多選擇。

  • 第一,它可以持續的檢查共享資料標誌(用於做保護工作的互斥量),直到另一執行緒完成工作時對這個標誌進行重設。不過,就是一種浪費:執行緒消耗寶貴的執行時間持續的檢查對應標誌,並且當互斥量被等待執行緒上鎖後,其他執行緒就沒有辦法獲取鎖,這樣執行緒就會持續等待。
  • 第二個選擇l是週期輪詢,在等待執行緒在檢查間隙,使用 std::this_thread::sleep_for() 進行週期性的間歇在這個迴圈中,在休眠前,函式對互斥量進行解鎖,並且在休眠結束後再對互斥量進行上鎖,所以另外的執行緒就有機會獲取鎖並設定標識。
  • 第三個選擇(也是優先的選擇)是,使用C++標準庫提供的條件變數condition_variable去等待事件的發生
    。通過另一執行緒觸發等待事件的機制是最基本的喚醒方式。

condition_variable

  std::condition_variable 和 std::condition_variable_any 。這兩個實現都包含在 <mutex> 或者<condition_variable>標頭檔案的宣告中。兩者都需要與一個互斥量一起才能工作(互斥量是為了同步);std::condition_variable僅限於與 std::mutex 一起工作,而後者可以和任何滿足最低標準的互斥量一起工作,從而加上了_any的字尾。因為 std::condition_variable_any 更加通用,這就可能從體積、效能,以及系統資源的使用方面產生額外的開銷。

wait()和notify_one()

1 std::mutex mymutex1;
2 std::unique_lock<std::mutex> sbguard1(mymutex1);
3 std::condition_variable condition;
4 condition.wait(sbguard1, [this] {if (!msgRecvQueue.empty())
5                                     return true;
6                                 return false
; 7 }); 8 9 condition.wait(sbguard1);

   wait()用來等一個事件或者條件滿足,如果第二個引數(可調物件)的lambda表示式返回值是false,即條件不滿足,那麼wait()將解鎖互斥量,並阻塞到本行,如果第二個引數的lambda表示式返回值是true,那麼wait()直接返回並繼續執行。

  阻塞到什麼時候為止呢?阻塞到其他某個執行緒呼叫notify_one()成員函式喚醒為止

  如果沒有第二個引數,那麼效果跟第二個引數lambda表示式返回false效果一樣

  wait()將解鎖互斥量,並阻塞到本行,直到到其他某個執行緒呼叫notify_one()成員函式為止。

  當其他執行緒用notify_one()將本執行緒wait()喚醒後,這個wait被喚醒後

  1、wait()不斷嘗試獲取互斥量鎖,如果獲取不到那麼流程就卡在wait()這裡等待獲取,如果獲取到了,那麼wait()就繼續執行,獲取到了鎖

  2.1、如果wait有第二個引數就判斷這個lambda表示式。

    a)如果表示式為false,那wait又對互斥量解鎖,然後又休眠,等待再次被notify_one()喚醒
    b)如果lambda表示式為true,則wait返回,流程可以繼續執行(此時互斥量已被鎖住)。
  2.2、如果wait沒有第二個引數,則wait返回,流程走下去。

  流程只要走到了wait()下面則互斥量一定被鎖住了。

 1 #include <thread>
 2 #include <iostream>
 3 #include <list>
 4 #include <mutex>
 5 using namespace std;
 6  
 7 class A {
 8 public:
 9     void inMsgRecvQueue() {
10         for (int i = 0; i < 100000; ++i) 
11         {
12             cout << "inMsgRecvQueue插入一個元素" << i << endl;
13 
14             std::unique_lock<std::mutex> sbguard1(mymutex1);
15             msgRecvQueue.push_back(i); 
16             //嘗試把wait()執行緒喚醒,執行完這行,
17             //那麼outMsgRecvQueue()裡的wait就會被喚醒
18             //只有當另外一個執行緒正在執行wait()時notify_one()才會起效,否則沒有作用
19             condition.notify_one();
20         }
21     }
22  
23     void outMsgRecvQueue() {
24         int command = 0;
25         while (true) {
26             std::unique_lock<std::mutex> sbguard2(mymutex1);
27             // wait()用來等一個東西
28             // 如果第二個引數的lambda表示式返回值是false,那麼wait()將解鎖互斥量,並阻塞到本行
29             // 阻塞到什麼時候為止呢?阻塞到其他某個執行緒呼叫notify_one()成員函式為止;
30             //當 wait() 被 notify_one() 啟用時,會先執行它的 條件判斷表示式 是否為 true,
31             //如果為true才會繼續往下執行
32             condition.wait(sbguard2, [this] {
33                 if (!msgRecvQueue.empty())
34                     return true;
35                 return false;});
36             command = msgRecvQueue.front();
37             msgRecvQueue.pop_front();
38             //因為unique_lock的靈活性,我們可以隨時unlock,以免鎖住太長時間
39             sbguard2.unlock(); 
40             cout << "outMsgRecvQueue()執行,取出第一個元素" << endl;
41         }
42     }
43  
44 private:
45     std::list<int> msgRecvQueue;
46     std::mutex mymutex1;
47     std::condition_variable condition;
48 };
49  
50 int main() {
51     A myobja;
52     std::thread myoutobj(&A::outMsgRecvQueue, &myobja);
53     std::thread myinobj(&A::inMsgRecvQueue, &myobja);
54     myinobj.join();
55     myoutobj.join();
56 }

  上面的程式碼可能導致出現一種情況:因為outMsgRecvQueue()與inMsgRecvQueue()並不是一對一執行的,所以當程式迴圈執行很多次以後,可能在msgRecvQueue 中已經有了很多訊息,但是,outMsgRecvQueue還是被喚醒一次只處理一條資料。這時可以考慮把outMsgRecvQueue多執行幾次,或者對inMsgRecvQueue進行限流。

notify_all()

  同時通知所有等待執行緒,但是需要注意的是,如果所有執行緒只有一個執行緒可以拿到互斥量,那麼也只有一個執行緒可以繼續執行。

  對使用讀寫鎖的多個讀執行緒,可以同時被喚醒並同時繼續工作。

拓展

  當等待一個一次性事件時,condition_variable顯然不是最好的選擇,這時需要的是future。