Wiki
condition_variable
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
std::condition_variable 用於讓一個執行緒等待某個共享條件發生變化,而不是不停迴圈檢查。
典型場景包括:
- 生產者把資料放入佇列,消費者沒有資料時等待;
- 一個執行緒等待初始化完成;
- 一個工作執行緒等待新任務到來;
- 多個執行緒等待某個狀態變為 ready。
條件變數最重要的不是記住 wait() 和 notify_one(),而是理解它必須圍繞下面三件東西一起使用:
共享状态 + mutex + condition_variable
1.為什麼不能一直輪詢
最直接的等待方式可能是:
while (!ready)
{
}
這種寫法有兩個問題:
- 如果
ready是普通變數,多執行緒讀寫本身就可能產生資料競爭; - 即使用
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() 必須完成一組特殊操作:
- 當前執行緒已經持有 mutex;
wait()在睡眠前臨時釋放 mutex;- 執行緒被喚醒後重新獲取 mutex;
- 獲得 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();
即:
- 持鎖修改共享狀態;
- 釋放鎖;
- 再通知。
這樣等待執行緒醒來後更有機會直接獲得 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()可以實現超時等待。- 生產者消費者中,應在鎖內快速取出任務,在鎖外執行耗時工作。