跳到正文

Wiki

condition_variable

約 6 分鐘閱讀

本文由簡體中文內容確定性轉換,並受版本化術語表保護。

std::condition_variable 用於讓一個執行緒等待某個共享條件發生變化,而不是不停迴圈檢查。

典型場景包括:

  • 生產者把資料放入佇列,消費者沒有資料時等待;
  • 一個執行緒等待初始化完成;
  • 一個工作執行緒等待新任務到來;
  • 多個執行緒等待某個狀態變為 ready。

條件變數最重要的不是記住 wait()notify_one(),而是理解它必須圍繞下面三件東西一起使用:

共享状态 + mutex + condition_variable

1.為什麼不能一直輪詢

最直接的等待方式可能是:

while (!ready)
{
}

這種寫法有兩個問題:

  1. 如果 ready 是普通變數,多執行緒讀寫本身就可能產生資料競爭;
  2. 即使用 atomic,這種忙等也會持續佔用 CPU。

例如:

while (!ready.load())
{
}

在等待時間較長時通常不是理想方案。

條件變數允許執行緒進入阻塞等待,等到其他執行緒通知後再繼續。

2.基本組成

通常會同時存在:

std::mutex mutex;
std::condition_variable cv;
bool ready = false;

這裡:

  • ready:真正表示業務條件的共享狀態;
  • mutex:保護 ready
  • cv:負責睡眠和喚醒等待執行緒。

注意:

condition_variable 本身不是狀態。

它只是通知機制。真正決定執行緒能否繼續的是謂詞所檢查的共享狀態。

3.最簡單的等待與通知

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mutex;
std::condition_variable cv;
bool ready = false;

void worker()
{
    std::unique_lock<std::mutex> lock(mutex);

    cv.wait(lock, [] {
        return ready;
    });

    std::cout << "worker start\n";
}

int main()
{
    std::thread t(worker);

    {
        std::lock_guard<std::mutex> lock(mutex);
        ready = true;
    }

    cv.notify_one();
    t.join();
}

執行邏輯:

worker 获得 mutex

检查 ready == false

wait 暂时释放 mutex,并进入等待

main 获得 mutex

修改 ready = true

main 释放 mutex

notify_one

worker 被唤醒并重新获得 mutex

再次检查 ready

条件成立,wait 返回

4.為什麼 wait() 要配合 std::unique_lock

常見寫法是:

std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, predicate);

而不是:

std::lock_guard<std::mutex>

原因在於 wait() 必須完成一組特殊操作:

  1. 當前執行緒已經持有 mutex;
  2. wait() 在睡眠前臨時釋放 mutex;
  3. 執行緒被喚醒後重新獲取 mutex;
  4. 獲得 mutex 後再返回給呼叫者。

unique_lock 支援這種“暫時釋放、重新獲得”的所有權管理,而 lock_guard 不提供手動解鎖和重新加鎖能力。

5.wait(lock)wait(lock, predicate)

條件變數有兩種常見等待形式。

5.1.不帶謂詞

cv.wait(lock);

執行緒被喚醒後直接返回。

如果使用這種形式,就必須自己寫迴圈檢查條件:

while (!ready)
{
    cv.wait(lock);
}

5.2.帶謂詞

推薦寫:

cv.wait(lock, [] {
    return ready;
});

它可以理解為標準庫幫你寫了:

while (!ready)
{
    cv.wait(lock);
}

因此大多數普通場景優先使用帶謂詞版本。

6.虛假喚醒(spurious wakeup)

等待執行緒有可能在沒有對應業務事件發生時從 wait() 醒來,這叫虛假喚醒。

因此下面這種邏輯不可靠:

cv.wait(lock);
use_shared_data();

正確思路是:

每次醒來都重新檢查真正的共享條件。

也就是:

cv.wait(lock, [] {
    return ready;
});

這也是為什麼“謂詞”不是可有可無的裝飾。

7.條件變數沒有記憶:不要把 notify 當狀態

另一個常見誤解是:

呼叫了 notify_one(),以後某個執行緒再 wait() 時應該能收到這次通知。

不是。

條件變數不會像訊息佇列一樣永久儲存通知。

例如:

线程 A notify_one()

当时没有线程正在等待

这次通知结束

线程 B 之后才开始 wait()

執行緒 B 不會自動“補收到”之前的通知。

因此必須用共享狀態記錄事實:

ready = true;
cv.notify_one();

即使通知時沒有執行緒正在睡眠,未來執行緒進入:

cv.wait(lock, [] { return ready; });

也會先檢查到 ready == true,從而不需要睡眠。

這就是:

共享状态保存事实
condition_variable 负责提高等待效率

8.notify_one()notify_all()

8.1.notify_one()

cv.notify_one();

喚醒一個正在等待的執行緒。

適合:

  • 新增一個任務,只需要一個 worker 處理;
  • 只需要一個消費者繼續執行。

8.2.notify_all()

cv.notify_all();

喚醒所有正在等待的執行緒。

適合:

  • 全域性狀態改變,所有執行緒都需要重新檢查條件;
  • 程式停止,需要讓所有等待執行緒退出。

被喚醒不等於所有執行緒能同時進入臨界區。它們仍然需要競爭同一把 mutex。

9.修改狀態後什麼時候 notify

一種常見推薦寫法是:

{
    std::lock_guard<std::mutex> lock(mutex);
    ready = true;
}

cv.notify_one();

即:

  1. 持鎖修改共享狀態;
  2. 釋放鎖;
  3. 再通知。

這樣等待執行緒醒來後更有機會直接獲得 mutex,而不是醒來後馬上又因為通知執行緒仍持鎖而阻塞。

不過關鍵正確性原則不是“notify 必須永遠寫在鎖外”,而是:

對共享謂詞狀態的訪問必須正確同步,等待方必須在鎖保護下檢查謂詞。

10.生產者消費者

這是條件變數最經典的應用。

#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
#include <thread>

std::queue<int> queue;
std::mutex mutex;
std::condition_variable cv;
bool finished = false;

void producer()
{
    for (int value = 1; value <= 5; ++value)
    {
        {
            std::lock_guard<std::mutex> lock(mutex);
            queue.push(value);
        }

        cv.notify_one();
    }

    {
        std::lock_guard<std::mutex> lock(mutex);
        finished = true;
    }

    cv.notify_all();
}

void consumer()
{
    while (true)
    {
        int value = 0;

        {
            std::unique_lock<std::mutex> lock(mutex);

            cv.wait(lock, [] {
                return !queue.empty() || finished;
            });

            if (queue.empty() && finished)
            {
                break;
            }

            value = queue.front();
            queue.pop();
        }

        // 真正处理数据放在锁外
        std::cout << "consume " << value << '\n';
    }
}

int main()
{
    std::thread p(producer);
    std::thread c(consumer);

    p.join();
    c.join();
}

這裡謂詞不是單純:

!queue.empty()

而是:

!queue.empty() || finished

否則生產者徹底結束後,如果佇列已經為空,消費者可能永遠繼續等待,再也沒人通知它有資料。

11.為什麼取出任務後要儘快釋放鎖

消費者應該在鎖內只做:

检查队列
取出任务
修改共享状态

真正耗時的任務執行應儘量放到鎖外。

如果消費者拿著 queue 的 mutex 執行一個耗時 2 秒的任務,那麼生產者和其他消費者這 2 秒內都可能無法訪問佇列。

12.超時等待:wait_for()

cv.wait_for(lock, duration)

可以等待一段時間。

推薦同樣使用謂詞版本:

bool ok = cv.wait_for(
    lock,
    std::chrono::seconds(1),
    [] {
        return ready;
    });

返回:

  • true:謂詞最終成立;
  • false:超時且謂詞仍未成立。

示例:

if (!cv.wait_for(lock, std::chrono::seconds(1), [] {
        return ready;
    }))
{
    std::cout << "timeout\n";
}

13.絕對時間等待:wait_until()

cv.wait_until(lock, time_point, predicate)

適合已經有一個明確截止時間點的場景。

例如:

auto deadline = std::chrono::steady_clock::now()
              + std::chrono::seconds(2);

cv.wait_until(lock, deadline, [] {
    return ready;
});

涉及超時時,通常優先使用 steady_clock,因為它不會受到系統牆上時鐘被手動修改的影響。

14.一個條件變數可以等待多個條件嗎?

可以。

真正決定等待邏輯的是謂詞,例如:

cv.wait(lock, [] {
    return !queue.empty() || stopped || error;
});

醒來後再根據具體狀態分支處理:

if (error)
{
    ...
}
else if (stopped && queue.empty())
{
    ...
}
else
{
    ...
}

條件變數只是提示“相關狀態可能變化了”,最終必須重新檢查狀態本身。

15.std::condition_variable_any

普通:

std::condition_variable

主要配合:

std::unique_lock<std::mutex>

標準庫還提供:

std::condition_variable_any

它可以配合更廣泛的 BasicLockable 鎖型別,靈活性更高,但一般也可能帶來更多開銷。

普通 std::mutex 場景優先使用 std::condition_variable

C++20 中,condition_variable_any 還提供了能與 std::stop_token 配合的等待過載,這在 std::jthread 協作式停止裡很有用。

16.條件變數與 atomic 怎麼選

如果只是:

等待一个原子值改变

C++20 可以考慮:

atomic.wait()
atomic.notify_one()
atomic.notify_all()

如果條件涉及:

  • 佇列是否為空;
  • 多個欄位組合;
  • 複雜業務狀態;

通常:

mutex + condition_variable + predicate

更自然。

17.常見錯誤

17.1.錯誤 1:把通知本身當成狀態

cv.notify_one();

不會永久儲存一條“通知訊息”。必須有共享狀態記錄事實。

17.2.錯誤 2:不用謂詞處理虛假喚醒

cv.wait(lock);
use_data();

應該重新檢查條件。

17.3.錯誤 3:修改謂詞狀態時沒有使用同一套同步規則

等待方在 mutex 下讀取 ready,修改方卻在沒有 mutex 的情況下寫普通 bool ready,仍然可能是資料競爭。

17.4.錯誤 4:持鎖執行耗時任務

佇列取任務後應儘量解鎖,再處理任務。

17.5.錯誤 5:程式停止時只修改 finished,卻忘記通知等待執行緒

等待執行緒可能一直睡眠。停止路徑通常要配合 notify_all()

17.6.錯誤 6:只檢查 queue.empty(),沒有設計“不會再產生資料”的結束狀態

生產者消費者模型必須考慮執行緒如何正常退出。

18.小結

  • condition_variable 必須圍繞“共享狀態 + mutex + 謂詞”理解。
  • wait() 在睡眠時會臨時釋放 mutex,醒來後重新獲得,因此通常配合 unique_lock
  • 條件變數允許虛假喚醒,所以應始終重新檢查真正的條件。
  • notify_one() 喚醒一個等待者,notify_all() 喚醒全部等待者。
  • 條件變數不會永久儲存通知,真正的狀態必須由受同步保護的資料記錄。
  • wait_for() / wait_until() 可以實現超時等待。
  • 生產者消費者中,應在鎖內快速取出任務,在鎖外執行耗時工作。