Wiki
semaphore、latch 與 barrier
本文由簡體中文內容確定性轉換,並受版本化術語表保護。
C++20 在:
<semaphore>
<latch>
<barrier>
中加入了幾種更高層的同步工具:
std::counting_semaphore
std::binary_semaphore
std::latch
std::barrier
它們和 mutex 解決的問題不一樣。
可以先用一句話記住:
mutex
→ 一次只允许一个线程进入临界区
semaphore
→ 一共有多少张“通行证”
latch
→ 等大家一次性全部到齐
barrier
→ 每一轮都要等大家到齐后再进入下一轮
所以:
semaphore
→ 许可数量控制
latch
→ 一次性同步
barrier
→ 可重复阶段同步
這些工具很多都可以用:
mutex + condition_variable + counter
手動實現。
但既然標準庫已經提供了現成抽象,就沒必要每次自己重新設計狀態、計數和通知協議。
1.semaphore:許可數量
1.1.semaphore:控制“許可數量”
1.2.std::counting_semaphore
std::counting_semaphore 內部維護一個計數。
這個計數可以理解成:
當前還有多少張可用通行證。
例如:
当前计数 = 3
就可以理解成:
目前还有 3 个线程可以获得许可
線程想進入受限制區域之前調用:
semaphore.acquire();
如果當前計數:
> 0
那麼:
计数减 1
线程继续执行
如果當前計數:
== 0
那麼:
暂时没有许可
线程阻塞等待
等使用完以後:
semaphore.release();
表示:
歸還一個許可。
於是:
计数加 1
並且可能喚醒一個正在等待許可的線程。
所以 semaphore 最核心的流程就是:
acquire()
→ 拿一张通行证
执行受限制的工作
release()
→ 把通行证还回去
1.3.counting_semaphore 最典型的用途:限制併發數量
例如:
最多隻允許 2 個線程同時執行某段任務。
#include <chrono>
#include <print>
#include <semaphore>
#include <thread>
#include <vector>
using namespace std::chrono_literals;
std::counting_semaphore<2> slots(2);
void worker(int id)
{
slots.acquire();
std::println("start {}", id);
std::this_thread::sleep_for(500ms);
std::println("end {}", id);
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();
}
}
可能輸出:
start 0
start 1
end 0
start 2
end 1
start 3
end 2
start 4
end 3
end 4
雖然我們創建了:
5 个线程
但:
std::counting_semaphore<2> slots(2);
初始只有 2 個許可。
所以最多隻有:
2 个线程
能同時處於:
slots.acquire();
...
slots.release();
之間。
可以理解成:
一开始有 2 张通行证
线程 A 拿走一张
剩 1 张
线程 B 拿走一张
剩 0 张
线程 C 来了
没票
只能等
A 或 B 归还一张
↓
C 获得许可继续
1.4.semaphore 和 mutex 最大的區別
mutex 通常表達:
同一時刻只能有一個線程進入臨界區。
而:
std::counting_semaphore<N>
可以表達:
同一時刻最多允許 N 個線程進入。
所以:
mutex
≈ 只有 1 把钥匙
counting_semaphore
≈ 有 N 张通行证
但它們還有一個更重要的語義區別:
mutex
→ 强调锁的所有权
semaphore
→ 强调许可数量
對於 mutex:
谁 lock
通常就应该由谁 unlock
而 semaphore:
谁 acquire
不要求必须由同一个线程 release
也就是説:
semaphore 的許可可以由一個線程獲取,由另一個線程歸還。
這使它不僅可以限制併發數量,也可以做線程間信號通知。
1.5.模板參數和初始計數不是一回事
例如:
std::counting_semaphore<10> semaphore(3);
這裏:
10
不是:
当前有 10 个许可
它表示的是:
這個 semaphore 所支持的最大計數上界至少為 10。
而:
3
才是:
初始計數。
所以這個對象剛創建時:
semaphore.acquire();
semaphore.acquire();
semaphore.acquire();
前三次可以直接成功。
第四次:
semaphore.acquire();
就需要等待別人:
semaphore.release();
所以記住:
std::counting_semaphore<10> semaphore(3);
10
→ 计数上限相关参数
3
→ 当前初始许可数量
不要把它們混在一起。
1.6.try_acquire()
如果你不想:
没有许可就一直阻塞
可以用:
if (semaphore.try_acquire())
{
// 成功获得许可
semaphore.release();
}
它的邏輯是:
有许可
→ 拿走一个
→ 返回 true
没许可
→ 立刻返回 false
→ 不等待
這和:
mutex.try_lock();
很像。
1.7.帶超時的 acquire
還可以:
semaphore.try_acquire_for(...);
或者:
semaphore.try_acquire_until(...);
可以理解成:
try_acquire_for()
→ 最多等一段时间
try_acquire_until()
→ 最多等到某个时间点
例如:
最多等 100ms
拿到许可
→ 继续
100ms 还没拿到
→ 返回失败
→ 执行备用逻辑
適合:
资源池暂时没资源
连接数已满
并发额度暂时耗尽
不能无限等待
這類場景。
1.8.std::binary_semaphore
C++20 還提供:
std::binary_semaphore
它可以理解成:
計數最多隻有 1 的 semaphore。
也就是它只有兩種狀態:
0
→ 没有许可
1
→ 有一个许可
例如:
std::binary_semaphore signal(0);
初始:
0 个许可
所以:
signal.acquire();
會阻塞。
另一個線程執行:
signal.release();
以後:
许可变成 1
↓
一个等待线程可以继续
1.9.binary_semaphore 可以理解成一個“門”
例如:
std::binary_semaphore signal(0);
可以想成:
门一开始是关着的
線程 A:
signal.acquire();
相當於:
门没开
→ 在门口等
線程 B:
signal.release();
相當於:
发放一个许可
→ 让一个等待线程通过
所以 binary semaphore 很適合表達:
线程 A 等一个信号
线程 B 发出信号
1.10.binary_semaphore 不是 mutex
雖然 binary semaphore 的計數最多為 1,看起來很像:
std::mutex
但它們不是一回事。
mutex:
强调所有权
哪个线程获得锁
通常由哪个线程释放
而 binary_semaphore:
强调“有没有许可”
释放许可的线程
可以不是 acquire 的那个线程
可以這樣記:
mutex
→ 谁锁门,谁开门
semaphore
→ 系统里有几张通行证
所以不要因為:
binary_semaphore 的值只有 0 / 1
就把它當成 mutex 的完全替代品。
1.11.semaphore 不會自動讓共享對象線程安全
例如:
std::vector<int> data;
多個線程同時:
data.push_back(...);
可能產生數據競爭。
即使你用了:
std::counting_semaphore<4> semaphore(4);
它也不會神奇地讓:
std::vector
變成線程安全。
semaphore 只解決:
最多允许多少个线程获得许可
並不自動解決:
共享对象的一致性保护
所以很常見的組合是:
semaphore
→ 控制最多多少个任务同时运行
mutex
→ 保护任务访问的共享数据结构
兩者職責完全不同。
1.12.acquire 以後一定要記得 release
例如:
semaphore.acquire();
// 工作
semaphore.release();
如果中間發生:
return
异常
复杂分支
導致:
release()
沒有執行,那麼:
许可被拿走以后永远没有归还
可用許可會越來越少。
例如本來:
2 个许可
漏掉一次 release 後:
永远只剩 1 个
再漏一次:
永远变成 0
後續線程全部可能卡住。
所以 semaphore 也要認真考慮:
异常安全
提前 return
许可归还
2.latch:一次性同步
2.1.latch:一次性倒計時同步
std::latch 可以理解成:
一個只能使用一次的倒計時門。
例如:
std::latch done(3);
表示:
初始计数 = 3
每完成一個參與者:
done.count_down();
計數減 1。
過程類似:
3
↓
2
↓
1
↓
0
當計數變成:
0
等待 latch 的線程就可以繼續。
2.2.latch 最適合表達什麼
它最適合這種需求:
等 N 件事情全部做完,然後繼續。
例如:
启动 3 个初始化任务
任务 1 完成
→ count_down()
任务 2 完成
→ count_down()
任务 3 完成
→ count_down()
计数变成 0
↓
主线程继续
2.3.latch 示例
#include <latch>
#include <print>
#include <thread>
#include <vector>
std::latch done(3);
void worker(int id)
{
std::println("worker {} done", id);
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::println("all workers reached the latch");
for (auto& thread : threads)
{
thread.join();
}
}
可能輸出:
worker 1 done
worker 0 done
worker 2 done
all workers reached the latch
前三行順序不確定。
但:
all workers reached the latch
一定發生在:
3 次 count_down()
全部完成以後。
所以主線程根本不關心:
哪个 worker 先完成
它只關心:
计数归零了吗?
2.4.count_down()
done.count_down();
表示:
我完成了自己的這一份工作,把計數減掉。
例如:
初始 3
第一个线程 count_down()
→ 2
第二个线程 count_down()
→ 1
第三个线程 count_down()
→ 0
當計數到:
0
latch 被永久打開。
2.5.wait()
done.wait();
表示:
如果 latch 計數還沒有歸零,就等待。
如果此時:
count > 0
線程阻塞。
如果已經:
count == 0
那麼:
直接返回
而且 latch 一旦變成 0:
以后一直保持“打开”
不會重新關上。
2.6.arrive_and_wait()
如果當前線程:
既是参与者
又要等待其他参与者
可以使用:
latch.arrive_and_wait();
它可以理解成:
我已经到达
↓
计数减一
↓
然后我也等待其他人到达
也就是類似:
latch.count_down();
latch.wait();
不過 arrive_and_wait() 表達意圖更直接。
2.7.latch 是一次性的
這是 latch 最重要的特點:
只能用一次
例如:
std::latch done(3);
最終:
3 → 2 → 1 → 0
到 0 以後:
不会重新回到 3
所以 latch 特別適合:
等待一批初始化任务完成
等待几个线程启动就绪
等待一次性并行任务全部结束
而不適合:
第一轮同步
第二轮同步
第三轮同步
...
這種場景應該用:
std::barrier
2.8.latch 可以理解成“一次性開門”
例如:
初始 latch(3)
还差 3 人
门关着
还差 2 人
门关着
还差 1 人
门关着
最后 1 人到达
计数变 0
门打开
以后永远保持打开
所以:
latch
≈ 一次性会合点
3.barrier:可重複階段同步
3.1.barrier:可重複的階段同步
std::barrier 解決的是:
多個線程需要一輪一輪同步。
例如有三個線程:
线程 A
线程 B
线程 C
每個線程都要執行:
第 1 阶段
↓
等待所有人完成
第 2 阶段
↓
等待所有人完成
第 3 阶段
這時:
std::barrier
非常合適。
3.2.barrier 可以理解成“每輪都要集合”
假設:
std::barrier sync_point(3);
表示:
每一轮都要等 3 个参与者到达
第一輪:
A 到了
B 到了
C 到了
↓
3 人到齐
↓
全部放行进入下一轮
然後 barrier 自動重新準備:
下一轮继续等 3 人
所以:
latch
→ 只集合一次
barrier
→ 每一轮都重新集合
3.3.barrier 示例
#include <barrier>
#include <print>
#include <thread>
#include <vector>
std::barrier sync_point(3);
void worker(int id)
{
std::println("phase 1: {}", id);
sync_point.arrive_and_wait();
std::println("phase 2: {}", id);
sync_point.arrive_and_wait();
std::println("phase 3: {}", id);
}
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();
}
}
可能輸出:
phase 1: 2
phase 1: 0
phase 1: 1
phase 2: 1
phase 2: 2
phase 2: 0
phase 3: 0
phase 3: 2
phase 3: 1
每個階段內部:
谁先打印
不確定。
但是一定滿足:
所有 phase 1 都完成
↓
才可能出现 phase 2
所有 phase 2 都完成
↓
才可能出现 phase 3
所以不會出現:
线程 A 已经进入 phase 2
但线程 B 连 phase 1 的 barrier 都还没到
3.4.arrive_and_wait()
barrier 最常見的操作:
sync_point.arrive_and_wait();
表示:
我已经完成这一阶段
↓
记录一次到达
↓
然后等其他参与者
當本階段所有參與者都到達以後:
所有等待线程一起进入下一阶段
所以:
arrive_and_wait();
非常符合:
“这一轮我做完了,现在等大家。”
這種語義。
3.5.barrier 會自動進入下一輪
這是它和 latch 最大的不同。
latch:
计数归零
↓
永久打开
↓
结束
barrier:
本轮所有人到达
↓
本轮完成
↓
自动开始下一轮
↓
重新等待参与者
所以:
latch
→ 一次性同步
barrier
→ 循环同步
3.6.completion function
std::barrier 還可以設置:
階段完成函數(completion function)
它會在:
本轮所有参与者都到达
以後執行一次。
概念上:
所有线程完成第 N 阶段
↓
completion function
↓
进入第 N+1 阶段
這適合:
所有线程完成本轮计算
↓
统一更新一次共享状态
↓
所有线程开始下一轮
例如並行算法可能需要:
第 1 轮:
每个线程算自己那部分
所有人到 barrier
completion:
交换 / 汇总本轮结果
第 2 轮:
基于新结果继续计算
3.7.completion function 不要做太重的事情
因為:
所有参与者都在等这个阶段真正结束
所以 completion function 如果:
执行很慢
做长时间 IO
sleep
等待其他锁
执行复杂阻塞操作
那麼所有參與者都會一起被拖住。
所以 completion function 更適合:
很短的状态更新
交换阶段数据
维护轮次
轻量汇总
3.8.arrive_and_drop()
假設 barrier 初始有:
3 个参与者
但某個線程執行完當前階段以後:
後面幾輪不再參加了。
這時不能直接:
退出线程
因為 barrier 還以為:
下一轮仍然要等 3 人
但實際上以後只有:
2 人
於是其他兩個線程永遠等不到第三個。
所以需要:
barrier.arrive_and_drop();
表示:
我完成了本轮到达
并且:
以后每一轮都不用再等我
也就是把未來參與者數量永久減掉。
4.選擇與對比
4.1.latch 和 barrier 最核心的區別
可以直接這樣記:
latch
→ 一次性的
barrier
→ 可重复的
例如:
4.1.1.latch
等 4 个模块初始化完成
↓
程序正式启动
初始化只發生一次。
所以:
std::latch
很合適。
4.1.2.barrier
4 个线程一起做第 1 轮
↓
集合
一起做第 2 轮
↓
集合
一起做第 3 轮
↓
集合
每一輪都要同步。
所以:
std::barrier
更合適。
4.2.semaphore、latch、barrier 怎麼選
可以直接看這張表:
| 需求 | 推薦工具 |
|---|---|
| 最多允許 N 個線程同時獲得資源 | std::counting_semaphore |
| 一個簡單的 0 / 1 許可或線程間信號 | std::binary_semaphore |
| 等 N 個參與者一次性全部完成 | std::latch |
| 多個線程每輪都要同步 | std::barrier |
| 保護共享對象一致性 | std::mutex |
| 等複雜共享條件成立 | std::condition_variable |
4.3.和 mutex 的區別
mutex 關注的是:
谁能进入临界区
例如:
std::scoped_lock lock(mutex);
表達:
这段共享数据同时只允许一个线程操作
而 semaphore 關注:
还有多少许可
latch 關注:
还有多少参与者没完成
barrier 關注:
这一轮所有参与者到齐了吗
所以這些工具:
不是 mutex 的不同写法
而是:
解决不同同步模式
4.4.和 condition_variable 的關係
理論上很多同步器都可以自己手搓。
例如 latch 可以大概用:
counter
mutex
condition_variable
實現:
counter 每完成一个任务减 1
counter == 0
↓
notify_all()
barrier 也可以自己維護:
当前轮次
参与者数量
已到达数量
condition_variable
實現。
但這很容易產生:
计数错误
边界条件错误
通知丢失
轮次状态错误
异常退出后人数不一致
所以:
condition_variable
→ 通用零件
semaphore / latch / barrier
→ 已经封装好的常见同步模式
如果標準庫已經有你想表達的模型:
优先使用标准抽象
通常會讓代碼:
更短
更清楚
更不容易写错
更容易让别人看懂
4.5.semaphore 的典型場景
counting_semaphore 常見於:
限制同时下载任务数量
限制数据库连接数
限制 GPU / 加速器任务并发数
资源池可用对象数量
限制同时进行的网络请求
控制同时执行的重任务数量
例如:
线程可以有 100 个
但数据库连接池只有 10 个连接
→ semaphore 初始许可 = 10
這就很自然。
4.6.latch 的典型場景
latch 常見於:
多个模块并行初始化
主线程等待所有初始化结束
多个并行任务全部完成后统一继续
启动多个 worker,等待全部 ready
關鍵詞是:
一次性
4.7.barrier 的典型場景
barrier 常見於:
并行数值计算
迭代算法
分块处理
多线程仿真
每一轮都依赖上一轮完整结果
關鍵詞是:
一轮一轮同步
5.常見錯誤
5.1.錯誤:把 semaphore 當 mutex
錯誤理解:
用了 semaphore
→ 共享 vector 自动线程安全
不對。
semaphore 只控制:
许可数量
共享對象的一致性仍然可能需要:
std::mutex
5.2.錯誤:acquire 以後忘記 release
例如:
semaphore.acquire();
if (error)
{
return; // 忘了 release
}
這樣會永久損失一個許可。
5.3.錯誤:認為 binary semaphore 有 mutex 的所有權規則
binary semaphore 雖然只有:
0 / 1
但它仍然:
不是 mutex
許可可以由不同線程 acquire / release。
5.4.錯誤:把 latch 當成可以重置的工具
std::latch:
只能归零一次
歸零以後不會:
重新恢复初始计数
需要循環同步時用:
std::barrier
5.5.錯誤:barrier 的參與人數寫錯
例如:
std::barrier barrier(4);
但實際上只有:
3 个线程
會調用:
arrive_and_wait();
那麼:
永远差第 4 个参与者
所有線程都可能永久卡住。
5.6.錯誤:線程退出了,卻沒 arrive_and_drop()
如果某個 barrier 參與者:
以后不再参加后续轮次
卻直接退出,那麼 barrier 仍然會:
在后续每一轮等它
應該:
arrive_and_drop();
5.7.錯誤:completion function 太慢
completion function 執行期間:
所有参与者都在等阶段完成
所以裏面不適合:
长时间 IO
sleep
复杂阻塞
重计算
危险的锁嵌套
6.最後總結
這三個 C++20 同步工具可以這樣記:
semaphore
→ 有几张通行证
latch
→ 一次性等大家到齐
barrier
→ 每一轮都等大家到齐
更具體一點:
std::counting_semaphore
→ 允许同时存在多个许可
std::binary_semaphore
→ 只有 0 / 1 个许可
std::latch
→ 一次性倒计时,归零后永久打开
std::barrier
→ 每轮所有参与者到达后,自动开始下一轮
它們和 mutex 的關係:
mutex
→ 保护共享状态
semaphore / latch / barrier
→ 表达更高层的同步模式
如果能用標準同步原語直接表達你的需求:
優先使用標準工具,而不是手寫
mutex + condition_variable + counter。
最後可以用一句話徹底區分:
mutex
→ “谁能进?”
semaphore
→ “还能进几个?”
latch
→ “这一次人到齐了吗?”
barrier
→ “这一轮人到齐了吗?”