目錄
前言
學習目標
1. 信號量(Semaphore)
示例:限制并發下載任務
2. 閂鎖(Latch)
示例:賽跑
3. 屏障(Barrier)
示例:圖像處理流水線
4. 常見坑與對策
5. 實踐作業
總結
前言
有時候寫多線程代碼,會感覺自己像個“交通警察”:
-
這里要限流,不能讓大家一擁而上;
-
那里要等所有人到齊,才能一起發車;
-
還有時候要分階段執行,上一階段沒完,下一階段不能亂跑。
C++20 就給我們準備了三個“神器”:信號量(semaphore)、閂鎖(latch)和屏障(barrier)。它們比 mutex
、condition_variable
更貼近并發算法的實際需求。今天我們就一起來拆解這三個工具。
學習目標
-
理解 信號量:限制并發數量的計數器。
-
理解 閂鎖:一次性同步點(大家到齊再走)。
-
理解 屏障:可重用的多階段同步工具。
-
掌握在實際場景中如何選擇正確的同步原語。
1. 信號量(Semaphore)
信號量其實很古老了,操作系統里早就有。它的直覺含義就是:有多少個“通行證”。
-
std::counting_semaphore<N>
:計數信號量,最多 N 張通行證。 -
std::binary_semaphore
:只有 0 和 1,相當于一個簡單開關。
工作原理:
-
acquire()
:拿一張票。如果沒有票,就等待。 -
release()
:放回一張票,別人可以用。
示例:限制并發下載任務
假設我們要同時下載 3 個文件,超過 3 個要排隊。
#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
#include <chrono>
using namespace std;counting_semaphore<3> sem(3); // 最多允許 3 個并發任務void download(int id) {sem.acquire(); // 拿到“許可證”cout << "線程 " << id << " 開始下載..." << endl;this_thread::sleep_for(chrono::seconds(2));cout << "線程 " << id << " 下載完成!" << endl;sem.release(); // 歸還“許可證”
}int main() {vector<thread> threads;for (int i = 0; i < 10; ++i)threads.emplace_back(download, i);for (auto &t : threads) t.join();
}
運行結果:同時只有 3 個任務在下載,其他線程排隊等待。
是不是就像“廁所蹲位有限,來晚了的同學只能等著”?😂
2. 閂鎖(Latch)
閂鎖就是“一次性的大門”:大家必須等到所有人都到齊,才能一起開門進入下一階段。
-
std::latch
只能用一次(單次同步點)。 -
常見場景:多個線程并行準備,等所有人準備好后,一起開始執行。
示例:賽跑
#include <iostream>
#include <thread>
#include <latch>
#include <vector>
using namespace std;latch start_line(3); // 等待 3 個選手void runner(int id) {cout << "運動員 " << id << " 就位" << endl;start_line.arrive_and_wait(); // 到齊才出發cout << "運動員 " << id << " 開跑!" << endl;
}int main() {vector<thread> threads;for (int i = 0; i < 3; ++i)threads.emplace_back(runner, i);for (auto &t : threads) t.join();
}
運行結果:所有運動員就位之后,才一起開跑。
3. 屏障(Barrier)
屏障就像閂鎖的“可重復版”。
-
std::barrier
支持 多輪同步。 -
每一輪,所有線程必須到齊,才能進入下一輪。
-
還可以在每一輪結束時執行一個“階段完成回調”。
示例:圖像處理流水線
我們有三張圖片,每張要經過三輪處理(預處理 → 濾鏡 → 保存)。
#include <iostream>
#include <thread>
#include <barrier>
#include <vector>
using namespace std;void process(int id, barrier<> &bar) {for (int stage = 1; stage <= 3; ++stage) {cout << "線程 " << id << " 執行第 " << stage << " 階段" << endl;bar.arrive_and_wait(); // 等所有人完成這個階段}
}int main() {barrier bar(3, []{ cout << "=== 一個階段完成 ===" << endl; });vector<thread> threads;for (int i = 0; i < 3; ++i)threads.emplace_back(process, i, ref(bar));for (auto &t : threads) t.join();
}
運行效果:
-
每個階段,所有線程都要等齊;
-
每輪結束,打印“階段完成”。
這就像三個人組隊打副本,必須等大家都打完當前關卡,才能進入下一關。
4. 常見坑與對策
工具 | 常見坑 | 對策 |
---|---|---|
信號量 | release() 次數和 acquire() 不匹配,導致線程永遠卡住或無限放行 | 保持配對,最好寫成 RAII 封裝 |
閂鎖 | 只能用一次,用完不能重置 | 多階段場景請用 barrier |
屏障 | 回調函數里阻塞,導致死鎖 | 回調必須盡快返回,不要做耗時操作 |
5. 實踐作業
-
修改“下載任務”的例子,把
counting_semaphore
改為binary_semaphore
,看看效果有什么不同。 -
用
latch
實現一個“多人開會”,所有人到齊后一起進入會議。 -
用
barrier
寫一個分階段矩陣運算程序,驗證每一階段都同步完成。
總結
今天我們學習了 C++20 的三大同步原語:
-
信號量:控制并發數量,就像限流閥門。
-
閂鎖:一次性同步點,大家到齊一起走。
-
屏障:可重用的多階段同步工具,常用于分階段算法。
它們比傳統的 mutex + condition_variable
更直觀,也更貼近實際并發場景。掌握好這三大工具,你就能寫出更優雅的并發代碼。