目录
前言
学习目标
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
更直观,也更贴近实际并发场景。掌握好这三大工具,你就能写出更优雅的并发代码。