【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。