std::counting_semaphore是c++20无锁计数信号量,初始化值须为≥0的编译期常量;acquire/release可跨线程调用,需防死锁、加超时、勿误用binary_semaphore别名,且不保证唤醒顺序。

std::counting_semaphore 是 C++20 原生、轻量、无锁的计数信号量,但不是万能互斥替代品,用错场景或参数会直接编译失败或逻辑死锁。
初始化值必须非负,且是编译期常量
构造 std::counting_semaphore 时传入的初始值必须是 ≥ 0 的字面量或 constexpr 表达式。传负数、变量、运行时计算值都会触发编译错误,比如:
-
std::counting_semaphore sem(-1);→ 编译失败:“non-negative value required” -
int n = 3; std::counting_semaphore sem(n);→ 编译失败:模板参数需为常量表达式 - 正确写法:
std::counting_semaphore sem(3);或constexpr int init = 2; std::counting_semaphore sem(init);
注意:模板参数(如 )是最大允许计数值,不是初始值;初始值由构造函数参数指定,且不能超过该上限。
acquire() 和 release() 不必成对出现在同一线程
这是信号量区别于 std::mutex 的关键设计。你完全可以在线程 A 中调用 release(),在线程 B 中调用 acquire() —— 它们通过共享的计数器通信,而非所有权。
- 典型误用:
acquire()后忘记release()→ 计数器持续为 0,其他线程永久阻塞 - 反模式:
acquire()和release()在同一线程内“配对”使用,却未配合临界区保护 → 失去同步意义 - 正确解耦场景:生产者线程
release()通知有新数据,消费者线程acquire()等待数据就绪
尤其注意:没有“自动 RAII 封装”,必须手动确保 release() 被执行(比如用 std::scoped_lock 思路自己封装,或配合 try_acquire_for() 防死锁)。
超时获取必须用 try_acquire_for() 或 try_acquire_until()
acquire() 是无条件阻塞的,一旦等待对象永远不 release(),线程就卡死。真实系统中必须加超时防护。
-
sem.try_acquire_for(std::chrono::milliseconds(500)):最多等 500ms,返回bool -
sem.try_acquire_until(std::chrono::steady_clock::now() + std::chrono::seconds(2)):等至绝对时间点 - 不要试图用
while(!sem.try_acquire()) std::this_thread::yield();轮询 —— 浪费 CPU,且不保证唤醒时机
注意:try_acquire()(无超时)只是立即返回成功/失败,不等待;它适合纯非阻塞路径,比如事件驱动中的快速试探。
binary_semaphore 是 counting_semaphore 的别名,别混用模板参数
std::binary_semaphore 是类型别名,等价于 std::counting_semaphore。但它不接受构造参数以外的模板参数 —— 下面写法全错:
-
std::binary_semaphore sem(1);→ 编译错误:“binary_semaphore is not a template” -
std::counting_semaphore sem(0);✅ 正确,但和std::binary_semaphore sem(0);等价 - 想限制“最多 N 个并发”,必须用
std::counting_semaphore<n></n>,而不是binary_semaphore
混淆这点会导致编译器报错“counting_semaphore is not a member of std”,尤其是旧编译器未启用 C++20 或遗漏 #include <semaphore></semaphore> 时,错误信息容易误导人去查模板参数而非编译环境。
最易被忽略的一点:信号量不保证唤醒顺序。多个线程同时在 acquire() 等待时,谁先被唤醒由实现决定(通常是 FIFO,但标准不保证),若业务逻辑依赖严格调度顺序(如 ABC 打印),必须靠多个信号量接力控制,单靠一个信号量无法实现。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











