(三、條件變量)

條件變量(Condition Variable)

條件變量是一種同步原語(Synchronization Primitive)用於多線程之間的通信,它可以阻塞一個或同時阻塞多個線程直到:

  • 收到來自其他線程的通知
  • 超時
  • 發生虛假喚醒(Spurious Wakeup)

C++11為條件變量提供了兩個類

  • std::condition_variable:必須與std::unique_lock配合使用
  • std::condition_variable_any:更加通用的條件變量,可以與任意類型的鎖配合使用,相比前者使用時會有額外的開銷

二者具有相同的成員函數

成員函數 說明
notify_one 通知一個等待線程
notify_all 通知全部等待線程
wait 阻塞當前線程直到被喚醒
wait_for 阻塞當前線程直到被喚醒或超過指定的等待時間(長度)
wait_until 阻塞當前線程直到被喚醒或到達指定的時間(點)

二者在線程要等待條件變量前都必須要獲得相應的鎖

條件變量為什麼叫條件變量?

  • 條件變量存在虛假喚醒的情況,因此在線程被喚醒後需要檢查條件是否滿足
  • 無論是notify_one或notify_all都是類似於發出脈衝信號,如果對wait的調用發生在notify之後是不會被喚醒的,所以接收者在使用wait等待之前也需要檢查條件(標識)是否滿足,另一個線程(通知者)在nofity前需要修改相應標識供接收者檢查

條件變量因此得名。

為什麼條件變量需要和鎖一起使用?

觀察std::condition_variable::wait函數,發現它的兩個重載都必須將鎖作為參數

void wait(std::unique_lock<std::mutex>& lock);
template< class Predicate >
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);

首先考慮wait函數不需要鎖作為參數的情況,下面的代碼中flag初始化為false,線程A將flag置為true並使用notify_one發出通知,線程B使用while循環在wait前後都會檢查flag,直到flag被置為true才往下執行。

// Thread A
{
    std::unique_lock lck(mt);
    flag = true;
}
cv.notify_one();

// Thread B
auto pred = []()
{
    std::unique_lock lck(mt);
    return flag;
};

while (!pred())
{
    cv.wait();
}

如果兩個線程的執行順序為:

  • 線程B檢查flag發現其值為false
  • 線程A將flag置為true
  • 線程A使用notify_one發出通知
  • 線程B使用wait進行等待

那麼線程B將不會被喚醒(即線程B沒有察覺到線程A發出的通知),這顯然不是程序員想要的結果,發生這種情況的根源在於線程B對條件的檢查和進入等待的中間是有空檔的。wait函數需要鎖作為參數正是為瞭解決這一問題的。

// Thread B
auto pred = []()
{
    return flag;
};
std::unique_lock lck(mt);

while (!pred())
{
    cv.wait(lck);
}

當線程B調用wait的時候會釋放傳入的鎖並同時進入等待,當被喚醒時會重新獲得鎖,因此只要保證線程A在修改flag的時候是正確加鎖的那麼就不會發生前面的這種情況。 使用wait函數的另一個重載時下面的代碼與上面的6~8行是等價的。

cv.wait(lck, pred);

不僅僅是C++,就博主所知道的語言但凡有條件變量的概念都必須與鎖配合使用。以C#、Java為例

  • C#
// Thread A
lock (obj)
{
    flag = true;
    System.Threading.Monitor.Pulse(obj);
}

// Thread B
lock (obj)
{
    while(!pred())
    {
        System.Threading.Monitor.Wait(obj);
    }
}
  • Java
// Thread A
synchronized(obj) {
    flag = true;
    obj.notify();
}

// Thread B
synchronized(obj) {
    while(!pred()) {
        obj.wait();
    }
}

C#與C++不同之處在於C#在Pulse或PulseAll的線程必須持有鎖,而C++的notify_one和notify_all則無所謂是否持有鎖。