?條件變數
與本文無關的知識聯絡:
一、call_once
- 函式模板,第一個引數為標記,第二個引數為要呼叫的函式名,如test()
- 功能:保證寫入第二個引數的函式(如test() )只能被呼叫一次。具備互斥量的能力,但互斥量消耗的資源少,更高效
- call_once(), 第一個引數的標記為:
std::once_flag
,實際上once_flag是一個結構體,記錄函式是否已呼叫過。
例子:
//用once_flag實現單例模式
std::once_flag flag;
class Singleton
{
public:
static void CreateInstance()//call_once保證其只被呼叫一次
{
instance = new Singleton;
}
//兩個執行緒同時執行到這裡,其中一個執行緒要等另外一個執行緒執行完畢
static Singleton * getInstance() {
call_once(flag, CreateInstance);
return instance;
}
private:
Singleton() {}
static Singleton *instance;
};
Singleton * Singleton::instance = NULL;
本文正題開始:
二、condition_variable
- 實際上是一個類,是一個和條件相關的類,說白了就是等待一個條件達成!這個類需要和互斥量配合工作,用的時候我們要生成這個類物件,下面都是該類中的方法。
2.1 wait()
先看例子:
std::mutex myMutex;
std::unique_lock<std::mutex> uniLock(myMutex);
std::condition_variable cv;
//帶兩個引數
cv.wait(uniLock, [this] {
if (!msgRecvQueue.empty()) return true;
return false;
});
//只有一個引數
cv.wait(uniLock);
- wait()用來等待一個東西,第一個引數是要操作的鎖,第二個引數是函式物件(不懂函式物件可以去百度理解,我們後面舉例中都採用lambda表示式)
- 功能:①如果第二個引數的lambda表示式返回值是false,那麼wait()將解鎖互斥量,並阻塞到本行;②如果第二個引數的lambda表示式返回值是true,那麼wait()直接返回並繼續執行。
- 如果沒有第二個引數,那麼效果跟第二個引數lambda表示式返回false效果一樣,wait()將解鎖互斥量,並阻塞到本行。
- 阻塞到其他某個執行緒呼叫notify_one()成員函式為止。
當其他執行緒用notify_one()或者notify_all() 將本執行緒wait()喚醒後,這個wait恢復後:
1、wait()不斷嘗試獲取互斥量鎖,如果獲取不到那麼流程就卡在wait()這裡等待獲取繼續獲取鎖,如果獲取到了,那麼wait()就繼續執行2
2.1、如果wait有第二個引數就判斷這個lambda表示式。
a)如果表示式為false,那wait又對互斥量解鎖,然後又休眠,等待再次被notify_one()喚醒
b)如果lambda表示式為true,則wait返回,流程可以繼續執行(此時互斥量已被鎖住)。
2.2、如果wait沒有第二個引數,則wait返回,流程走下去。
2.2 wait_for()
std::condition_variable::wait_for的原型有兩種:
//第一種不帶pre的
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
//第二種,帶有pred
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);
即我們可以寫三個引數,我們看看帶謂詞pre的版本(pre其實也就是wait的第二個引數),wait_for會阻塞其所線上程(該執行緒應當擁有lock),直至超過了rel_time,或者謂詞返回true。在阻塞的同時會自動呼叫lck.unlock()
讓出執行緒控制權。對上述行為進行總結:
- 只要謂詞返回true(阻塞期間只要notify了才會看謂詞狀態),立刻喚醒執行緒(返回值為true);
- 當謂詞為false,沒超時就繼續阻塞,超時了就喚醒(此時返回值為false);
- 當其他執行緒使用
notify_one
或者notify_all
進行喚醒時,取決於謂詞的狀態,若為false,則為虛假喚起,執行緒依然阻塞。
2.3 notify_one()
- 只喚醒等待佇列中的第一個執行緒,不存在鎖爭用(同佇列中不存在),所以能夠立即獲得鎖。其餘的執行緒不會被喚醒,需要等待再次呼叫
notify_one()
或者notify_all()
。
2.4 notify_all()
-
會喚醒所有等待佇列中阻塞的執行緒,存在鎖爭用,只有一個執行緒能夠獲得鎖,而其餘的會接著嘗試獲得鎖(類似輪詢)
tips: ,java必須在鎖內(與wait執行緒一樣的鎖)呼叫notify。但c++是不需要上鎖呼叫的,如果在鎖裡呼叫,可能會導致被立刻喚醒的執行緒繼續阻塞(因為鎖被notify執行緒持有)。c++標準在通知呼叫中,直接將等待執行緒從條件變數佇列轉移到互斥佇列,而不喚醒它,來避免此"hurry up and wait"場景。我在做多執行緒的題目的時候,notify_one 是在鎖內末尾的時候呼叫的。
程式碼的優化:
參考一下我的筆記中:拿出資料的函式:
第一種寫法:
//拿取資料的函式
bool outMsgPro(int& command) {
{
std::lock_guard<std::mutex> myGuard(myMutex);
if (!msgRecvQueue.empty()) {//非空就進行操作
command = msgRecvQueue.front();
msgRecvQueue.pop();
return true;
}
}
//其他操作程式碼
return false;
}
不管資訊接收佇列(msgRecvQueue)是不是空,都要加鎖解鎖,大大降低了效率------這個問題在設計模式中單例模式也有說明
進一步優化:第二種寫法(正常優化寫法)
//拿取資料的函式
bool outMsgPro(int& command) {
//雙重鎖定
if (!msgRecvQueue.empty()) {
std::lock_guard<std::mutex> myGuard(myMutex);
if (!msgRecvQueue.empty()) {//非空就進行操作
command = msgRecvQueue.front();
msgRecvQueue.pop();
return true;
}
}
//其他操作程式碼
return false;
}
再進一步優化寫法:第三種寫法
//拿取資料的函式
std::condition_variable cv;
void outMsgPro(int& command) {
std::lock_guard<std::mutex> myGuard(myMutex);
//採用wait方式,拿到資料
cv.wait(myGuard, [this] {
if (!msgRecvQueue.empty()) return true;
return false;
});
command = msgRecvQueue.front();
msgRecvQueue.pop();
myGuard.unlock();
//其他操作程式碼
return;
}