多執行緒06:條件變數

booker 發表於 2022-05-13

📕條件變數

與本文無關的知識聯絡:

一、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;
}

相關文章