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