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()可以實現超時等待。- 生產者消費者中,應在鎖內快速取出任務,在鎖外執行耗時工作。