C++11 條件變量到底有多強?五分鐘帶你徹底搞懂線程同步!
大家好啊,我是小康。今天咱們聊一個聽起來挺高深,但其實超實用的話題 —— C++11條件變量。
說實話,我第一次接觸這玩意兒時也是一臉懵逼:"條件變量?這不就是個變量嗎,有啥好講的?"
結果一看代碼,頓時傻眼了...
但別慌!今天我用最白話的方式幫你徹底搞懂它。不講那些晦澀的概念,就講你真正需要知道的東西。

一、條件變量到底是個啥?
想象你和朋友在肯德基排隊,但你突然想上廁所。
你對朋友說:"哥們,我去個衛生間,到咱們了你喊我一聲啊!"
然后你去衛生間了,但并不是一直站在那兒傻等,而是該干嘛干嘛去了。
這就是條件變量的核心思想:一個線程(你)在等待某個條件滿足(隊排到了),另一個線程(你朋友)負責在條件滿足時通知等待的線程(你)。
條件變量的厲害之處就是:它讓等待的線程能夠暫時"睡眠",不消耗CPU資源,直到被另一個線程喚醒。
二、為啥要用條件變量?
直接上一個生活中的例子:
假設你在煮方便面,說好了3分鐘熟。你有兩種等待方式:
- 傻等法:眼睛死盯著鍋和手表,不停地問自己"好了沒?好了沒?"(這就是所謂的"忙等待",特別浪費資源)
- 聰明等法:設個3分鐘鬧鐘,然后玩會手機,鬧鈴響了再去看鍋(這就是條件變量的思想)
顯然,聰明等法更高效,既不浪費你的注意力(CPU資源),事情也能圓滿完成。
三、條件變量的基本用法
C++11中,我們主要用到這兩個類:
- std::condition_variable - 就是我們的條件變量主角
- std::mutex - 它的好搭檔,互斥鎖
基本用法分 2 步:
- 等待條件滿足(等待方)
std::unique_lock<std::mutex> lock(mutex); // 先上鎖
while (!條件滿足) { // 檢查條件
cv.wait(lock); // 不滿足就等待(自動釋放鎖并休眠)
}
// 條件滿足了,繼續執行
// 鎖還在手里,記得用完放開- 滿足條件并通知(通知方)
{
std::lock_guard<std::mutex> lock(mutex); // 先上鎖
// 改變條件狀態
條件 = true;
} // 鎖自動釋放
cv.notify_one(); // 通知一個等待的線程
// 或
cv.notify_all(); // 通知所有等待的線程就這么簡單!
但是,光說不練假把式,來看個具體例子。
四、經典案例:生產者-消費者問題
我們用做早餐來解釋:
- 生產者:就是做煎餅的師傅(不斷地生產煎餅)
- 消費者:就是饑腸轆轆的食客(不斷地吃煎餅)
- 共享緩沖區:就是放煎餅的托盤(容量有限)
規則很簡單:
- 托盤滿了,師傅就得等等(生產者等待)
- 托盤空了,食客就得等等(消費者等待)
- 師傅做好一個,告訴食客可以吃了(生產者通知)
- 食客吃完一個,告訴師傅可以繼續做了(消費者通知)
代碼實現:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std;
// 共享數據及同步對象
queue<int> products; // 煎餅托盤
mutex mtx; // 互斥鎖
condition_variable cv_empty; // 托盤空了的條件變量
condition_variable cv_full; // 托盤滿了的條件變量
constint MAX_PRODUCTS = 5; // 托盤最多放5個煎餅
// 生產者線程(做煎餅的師傅)
void producer() {
for (int i = 1; i <= 10; ++i) { // 做10個煎餅
{
unique_lock<mutex> lock(mtx); // 先上鎖
// 如果托盤滿了,就等待
cv_empty.wait(lock, []{
return products.size() < MAX_PRODUCTS;
});
// 做一個煎餅,放到托盤上
products.push(i);
cout << "師傅做好第 " << i << " 個煎餅,托盤現在有 "
<< products.size() << " 個煎餅\n";
} // 解鎖
// 通知消費者有煎餅可以吃了
cv_full.notify_one();
// 做煎餅需要一點時間
this_thread::sleep_for(chrono::milliseconds(300));
}
}
// 消費者線程(吃煎餅的食客)
void consumer() {
for (int i = 1; i <= 10; ++i) { // 要吃10個煎餅
int product;
{
unique_lock<mutex> lock(mtx); // 先上鎖
// 如果托盤空了,就等待
cv_full.wait(lock, []{
return !products.empty();
});
// 從托盤取一個煎餅吃
product = products.front();
products.pop();
cout << "食客吃掉第 " << product << " 個煎餅,托盤還剩 "
<< products.size() << " 個煎餅\n";
} // 解鎖
// 通知生產者托盤有空位了
cv_empty.notify_one();
// 吃煎餅需要一點時間
this_thread::sleep_for(chrono::milliseconds(500));
}
}
int main() {
cout << "===== 煎餅店開張啦! =====\n";
thread t1(producer); // 啟動生產者線程
thread t2(consumer); // 啟動消費者線程
t1.join(); // 等待生產者線程結束
t2.join(); // 等待消費者線程結束
cout << "===== 煎餅賣完了! =====\n";
return 0;
}運行結果可能是這樣的:
===== 煎餅店開張啦! =====
師傅做好第 1 個煎餅,托盤現在有 1 個煎餅
食客吃掉第 1 個煎餅,托盤還剩 0 個煎餅
師傅做好第 2 個煎餅,托盤現在有 1 個煎餅
師傅做好第 3 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 2 個煎餅,托盤還剩 1 個煎餅
師傅做好第 4 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 3 個煎餅,托盤還剩 1 個煎餅
師傅做好第 5 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 4 個煎餅,托盤還剩 1 個煎餅
師傅做好第 6 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 5 個煎餅,托盤還剩 1 個煎餅
師傅做好第 7 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 6 個煎餅,托盤還剩 1 個煎餅
師傅做好第 8 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 7 個煎餅,托盤還剩 1 個煎餅
師傅做好第 9 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 8 個煎餅,托盤還剩 1 個煎餅
師傅做好第 10 個煎餅,托盤現在有 2 個煎餅
食客吃掉第 9 個煎餅,托盤還剩 1 個煎餅
食客吃掉第 10 個煎餅,托盤還剩 0 個煎餅
===== 煎餅賣完了! =====看到沒?師傅和食客配合得多默契啊!這就是條件變量的魅力:讓兩個線程之間能夠無縫協作。
五、條件變量的幾個關鍵點
1. 為什么要用 while 循環檢查條件?
也許你注意到了,示例代碼用的是 lambda 函數而不是 while 循環。但在老式寫法中,我們通常這樣:
while (!條件滿足) {
cv.wait(lock);
}不用 if 而用 while 的原因是:虛假喚醒。
有時候,等待的線程可能會在沒有人通知的情況下醒來(就像你睡覺時突然被樓上裝修吵醒)。如果用 if,線程會錯誤地認為條件已滿足;用 while,它會再檢查一遍,發現條件沒滿足,繼續等待。
2. wait() 的兩種用法
條件變量的 wait() 有兩種調用方式:
// 方式1:只傳遞鎖
cv.wait(lock);
// 方式2:傳遞鎖和判斷條件(推薦)
cv.wait(lock, []{ return 條件滿足; });方式 2 相當于:
while (!條件滿足) {
cv.wait(lock);
}但方式 2 更簡潔、更不容易出錯,強烈推薦使用!
3. 重要的超時等待函數
有時候,我們不想無限期等待,而是最多等待一段時間。C++11提供了超時版本的 wait 函數:
// 最多等待100毫秒
auto status = cv.wait_for(lock, chrono::milliseconds(100), []{ return 條件滿足; });
if (status) {
// 條件滿足了
} else {
// 超時了,條件仍未滿足
}這就像你等外賣:如果 30 分鐘送不到,我就自己做飯吃了!
六、高級案例:線程池中的任務調度
想象一個更復雜的例子:一個簡單的線程池。這是很多高性能系統的基礎設施:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
class ThreadPool {
private:
vector<thread> workers; // 工作線程
queue<function<void()>> tasks; // 任務隊列
mutex mtx; // 互斥鎖
condition_variable cv; // 條件變量
bool stop; // 停止標志
public:
// 構造函數,創建指定數量的工作線程
ThreadPool(size_t threads) : stop(false) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this] {
while (true) {
function<void()> task;
{
unique_lock<mutex> lock(this->mtx);
// 等待任務或停止信號
this->cv.wait(lock, [this] {
returnthis->stop || !this->tasks.empty();
});
// 如果線程池停止且沒有任務,則退出
if (this->stop && this->tasks.empty()) {
return;
}
// 獲取一個任務
task = move(this->tasks.front());
this->tasks.pop();
}
// 執行任務
task();
}
});
}
}
// 添加新任務到線程池
template<class F>
void enqueue(F&& f) {
{
unique_lock<mutex> lock(mtx);
// 不允許在線程池停止后添加任務
if (stop) {
throw runtime_error("ThreadPool已停止,無法添加任務");
}
tasks.emplace(forward<F>(f));
}
// 通知一個等待的線程有新任務
cv.notify_one();
}
// 析構函數,停止所有線程
~ThreadPool() {
{
unique_lock<mutex> lock(mtx);
stop = true;
}
// 通知所有等待的線程
cv.notify_all();
// 等待所有線程結束
for (auto& worker : workers) {
worker.join();
}
}
};
// 測試線程池
int main() {
// 創建4個工作線程的線程池
ThreadPool pool(4);
// 添加一些任務
for (int i = 1; i <= 8; ++i) {
pool.enqueue([i] {
cout << "任務 " << i << " 開始執行,線程ID: "
<< this_thread::get_id() << endl;
// 模擬任務執行時間
this_thread::sleep_for(chrono::seconds(1));
cout << "任務 " << i << " 執行完成" << endl;
});
}
// 主線程暫停一會兒,讓工作線程有時間執行任務
this_thread::sleep_for(chrono::seconds(10));
cout << "主線程退出" << endl;
return 0;
}運行結果可能是這樣的:
任務 1 開始執行,線程ID: 140271052129024
任務 2 開始執行,線程ID: 140271060521728
任務 3 開始執行,線程ID: 140271068914432
任務 4 開始執行,線程ID: 140271077307136
任務 1 執行完成
任務 5 開始執行,線程ID: 140271052129024
任務 2 執行完成
任務 6 開始執行,線程ID: 140271060521728
任務 3 執行完成
任務 7 開始執行,線程ID: 140271068914432
任務 4 執行完成
任務 8 開始執行,線程ID: 140271077307136
任務 5 執行完成
任務 6 執行完成
任務 7 執行完成
任務 8 執行完成
主線程退出看!多個線程自動分配任務,互不干擾,效率杠杠的!
七、條件變量使用的注意事項
(1) 永遠和互斥鎖一起使用:條件變量需要和互斥鎖配合,否則會導致競態條件
(2) 檢查喚醒原因:被喚醒不一定是因為條件滿足,所以總是要檢查條件(用while或wait的謂詞版本)
(3) 注意通知時機:通常先改變條件狀態,再發出通知,且通知應在解鎖后進行
(4) 區分 notify_one 和 notify_all:
- notify_one(): 只喚醒一個等待的線程(適合一對一通知)
- notify_all(): 喚醒所有等待的線程(適合廣播通知)
(5) 防止丟失喚醒:如果通知在等待之前發出,那么可能會丟失,導致線程永遠等待
八、總結:條件變量,讓你的多線程程序更高效!
條件變量就像多線程世界里的"微信群通知":讓線程之間能夠高效協調工作,不必浪費CPU資源去傻等。
關鍵知識點回顧:
- 條件變量用于線程間的等待/通知機制
- 必須與互斥鎖配合使用
- 使用 wait() 等待條件滿足
- 使用 notify_one()/notify_all() 通知等待的線程
- 總是在循環中檢查條件,防止假喚醒
掌握了條件變量,你的C++多線程技能就上了一個臺階!再也不用擔心線程間如何優雅地協作啦~
































