semaphore、latch 與 barrier
C++20 在 <semaphore>、<latch>、<barrier> 中加入了三類更高層的同步工具:
std::counting_semaphore
std::binary_semaphore
std::latch
std::barrier
它們解決的問題和 mutex 不同:
mutex:保護共享臨界區;semaphore:限制同時進入某個區域的線程數量,或表示可用資源數量;latch:一次性等待若干參與者全部到達;barrier:多個線程反覆進行“階段同步”。
這些工具不是併發入門第一優先級,但理解後可以避免手寫複雜的條件變量計數邏輯。
1. std::counting_semaphore
計數信號量內部維護一個計數值。
線程進入前調用:
semaphore.acquire();
如果計數大於 0:
计数减 1
线程继续
如果計數為 0:
线程阻塞等待
使用完資源後調用:
semaphore.release();
計數增加,並可能喚醒等待線程。
2. 限制併發數量
例如只允許最多兩個線程同時執行某段任務:
#include <chrono>
#include <iostream>
#include <semaphore>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
std::counting_semaphore<2> slots(2);
void worker(int id)
{
slots.acquire();
std::cout << "start " << id << '\n';
std::this_thread::sleep_for(500ms);
std::cout << "end " << id << '\n';
slots.release();
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i)
{
threads.emplace_back(worker, i);
}
for (auto& thread : threads)
{
thread.join();
}
}
即使創建了 5 個線程,真正進入受限區域的線程最多隻有 2 個。
3. 模板參數和初始計數不是同一個東西
std::counting_semaphore<10> semaphore(3);
其中:
10
表示實現至少支持的最大計數上界;
3
表示當前初始計數。
不要把兩者混在一起理解。
4. try_acquire()
如果不想阻塞,可以使用:
if (semaphore.try_acquire())
{
// 成功获得一个许可
semaphore.release();
}
還有帶時間限制的版本:
try_acquire_for(...)
try_acquire_until(...)
適合“等不到資源就做其他事情”的場景。
5. std::binary_semaphore
二值信號量只有兩種狀態,可以理解為計數最多為 1 的 semaphore。
標準庫提供別名:
std::binary_semaphore
例如:
std::binary_semaphore signal(0);
一個線程:
signal.acquire();
另一個線程:
signal.release();
它可以用於簡單的“一次允許一個等待者繼續”的同步。
不過二值信號量和 mutex 的語義仍然不同:
mutex 強調所有權:由獲得鎖的線程釋放;semaphore 強調許可數量,釋放許可的線程不一定是獲得許可的那個線程。
6. semaphore 不能自動保護複雜共享狀態
有了 semaphore 並不意味着共享容器就自動線程安全。
例如:
std::vector<int> data;
如果多個線程同時修改這個 vector,仍然需要正確同步。
semaphore 常用於:
控制并发数量
限制资源池容量
表示可用资源数
线程间发放许可
而不是簡單替代 mutex。
7. std::latch
std::latch 是一個一次性倒計時同步點。
創建時指定計數:
std::latch done(3);
參與者完成後調用:
done.count_down();
等待方調用:
done.wait();
當計數降到 0 後,所有等待者繼續執行。
8. latch 示例
#include <iostream>
#include <latch>
#include <thread>
#include <vector>
std::latch done(3);
void worker(int id)
{
std::cout << "worker " << id << " done\n";
done.count_down();
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i)
{
threads.emplace_back(worker, i);
}
done.wait();
std::cout << "all workers reached the latch\n";
for (auto& thread : threads)
{
thread.join();
}
}
這裏:
done.wait();
只關心“3 個參與者是否都已經完成某個階段”。
9. count_down()、wait()、arrive_and_wait()
常見操作:
latch.count_down();
latch.wait();
latch.arrive_and_wait();
arrive_and_wait() 可以理解為:
我已经到达
计数减一
然后我也等待其他参与者
10. latch 只能使用一次
這是 latch 和 barrier 最關鍵的區別。
一旦 latch 的計數變為 0:
它不会重新恢复到初始值
所以它適合:
- 等待多個初始化任務完成;
- 等待一組 worker 啓動就緒;
- 等待一次性並行任務全部結束。
如果程序需要一輪又一輪地同步,應使用 std::barrier。
11. std::barrier
std::barrier 是可重複使用的階段同步點。
假設有 3 個線程,每一輪都必須:
阶段 1 全部完成
↓
所有线程才能进入阶段 2
↓
阶段 2 全部完成
↓
所有线程才能进入阶段 3
這種場景非常適合 barrier。
12. barrier 示例
#include <barrier>
#include <iostream>
#include <thread>
#include <vector>
std::barrier sync_point(3);
void worker(int id)
{
std::cout << "phase 1: " << id << '\n';
sync_point.arrive_and_wait();
std::cout << "phase 2: " << id << '\n';
sync_point.arrive_and_wait();
std::cout << "phase 3: " << id << '\n';
}
int main()
{
std::vector<std::thread> threads;
for (int i = 0; i < 3; ++i)
{
threads.emplace_back(worker, i);
}
for (auto& thread : threads)
{
thread.join();
}
}
雖然每個階段內部的打印順序仍然不確定,但不會有某個線程提前跨過同步點進入下一階段,而其他線程還沒有完成上一階段。
13. barrier 會自動開始下一輪
當本輪所有參與者到達後,barrier 會完成當前階段,然後自動為下一階段重新準備。
這正是它與 latch 的不同:
latch
一次性
barrier
可重复阶段同步
14. completion function
std::barrier 還可以在每一階段全部參與者到達後執行一個完成函數。
概念上:
所有线程到达 barrier
↓
执行 completion function
↓
进入下一阶段
這適合每輪結束都需要統一做一次狀態更新的算法。
使用這個功能時應確保 completion function 本身不會引入新的複雜阻塞或鎖依賴。
15. arrive_and_drop()
如果某個參與者之後不再參加後續階段,可以調用:
barrier.arrive_and_drop();
表示:
我完成本輪到達,並且從以後各輪的參與者數量中永久退出。
這和簡單離開線程不同,因為 barrier 必須知道未來還應該等待多少參與者。
16. 三種工具怎麼選
| 問題 | 推薦工具 |
|---|---|
| 最多允許 N 個線程同時訪問資源 | counting_semaphore |
| 一個線程給另一個線程發一個簡單許可 | binary_semaphore |
| 等待 N 個一次性任務全部到達 | latch |
| 多個線程每一輪都要在階段邊界會合 | barrier |
| 保護共享對象的一致性 | mutex |
| 等待複雜謂詞條件 | condition_variable |
17. 和 condition_variable 的關係
這些工具很多都可以用:
mutex + condition_variable + counter
手工實現。
例如 latch 本質上就是一種:
计数递减到 0 后唤醒等待者
的同步模式。
但既然 C++20 已經提供標準抽象,就不必每次重新發明一套狀態變量和通知協議。
標準工具的優勢是:
- 意圖更明確;
- 代碼更短;
- 更不容易寫錯邊界條件;
- 閲讀代碼的人一眼能看出同步模式。
18. 常見錯誤
- 把 semaphore 當成 mutex,誤以為它自動保護共享對象。
acquire()後忘記release(),導致許可永久減少。- 認為 binary semaphore 具有 mutex 一樣嚴格的線程所有權語義。
- 把一次性的
latch當成可以重置循環使用的同步器。 - barrier 的參與者數量設計錯誤,導致永遠等不到所有參與者。
- 某個線程提前退出,卻沒有用
arrive_and_drop()調整 barrier 後續參與數量。 - 在 completion function 中執行復雜阻塞操作,讓所有參與者都卡在階段邊界。
小結
counting_semaphore用計數控制可同時獲得的許可數量。binary_semaphore是計數最多為 1 的信號量語義。latch是一次性倒計時同步點,計數歸零後永久打開。barrier是可重複使用的階段同步點。- 這些工具解決的是同步模式,不直接替代 mutex 對共享狀態的保護。
- C++20 項目遇到對應模式時,優先考慮標準同步原語,而不是手寫複雜條件變量計數器。