c++oding="utf-8" ?>
std::counting_semaphore是c++20标准库提供的现成计数信号量,模板参数指定最大计数值(编译期常量),构造值不得超限,支持acquire()、release()和try_acquire()等原子操作,不可替代mutex,无跨进程能力。

counting_semaphore 是 C++20 标准库里的现成类型,不用手写
如果你在查怎么“实现”它,大概率是想用信号量做资源计数(比如限制最多 3 个线程同时访问某服务),但其实 std::counting_semaphore 已经直接提供了这个能力——它是标准库中唯一支持非二进制计数的同步原语,底层通常基于 futex(Linux)或 SRW lock + event(Windows),无需自己封装原子变量加条件变量。
常见误区是试图用 std::atomic<int></int> 配合 std::condition_variable 手搓一个“看起来像”的版本,结果漏掉唤醒丢失、虚假唤醒、ABA 或内存序问题。标准实现已处理这些细节。
正确初始化和使用 counting_semaphore 的关键参数
构造时传入的初始值就是最大许可数(即资源上限),不是当前可用数——这点容易反直觉。例如 std::counting_semaphore sem(5) 表示最多允许 5 个线程同时 acquire,而不是“初始有 5 个空闲资源”。
-
sem.acquire():阻塞直到拿到一个许可,内部自动做 -- 操作(带 acquire 内存序) -
sem.try_acquire():非阻塞尝试,返回true表示成功减一,false表示当前无许可 -
sem.release(N):释放 N 个许可(可大于 1),内部做 += 操作(带 release 内存序),注意不会校验是否超过初始值——它允许“超额释放”,比如初始为 1,调用release(100)后计数变成 101
为什么不能用 binary_semaphore 替代 counting_semaphore
std::binary_semaphore 只能取值 0 或 1,本质是互斥锁的简化版;而 counting_semaphore 的模板参数(如 )指定的是底层计数器的位宽,影响最大可表示值(2^32-1),不决定初始值。两者 API 几乎一致,但语义不同:
- 用
binary_semaphore做“最多 3 个并发”必须配合额外状态管理(比如原子计数器 + 条件变量),逻辑变重且易错 -
counting_semaphore天然支持多资源复用场景,比如连接池、GPU kernel 并发数控制、限流器中的令牌桶补充 - 性能上,
counting_semaphore在多数实现中与binary_semaphore底层共享 fast-path 优化,无明显开销差异
容易被忽略的跨平台兼容性坑
虽然 C++20 标准定义了接口,但部分旧编译器或 STL 实现(如 libstdc++ 12 之前、MSVC 19.30 之前)未完全支持 counting_semaphore。检查方式很简单:
- 编译时报错
‘counting_semaphore’ is not a member of ‘std’→ 升级 STL 或启用实验性支持(GCC 加-D_GLIBCXX_CONCEPTS,Clang 加-stdlib=libc++并确认版本 ≥ 14) - 运行时发现
acquire()不阻塞或死锁 → 检查是否链接了 pthread(Linux)或正确初始化了 Windows 线程池(某些 MinGW 构建可能漏掉) - 调试时看到计数器值异常大(如 4294967295)→ 通常是未初始化或重复
release()导致整数溢出,建议始终用try_acquire()+ 日志兜底
真正难的从来不是“怎么写”,而是理解它不提供所有权转移、不绑定资源生命周期、也不保证 FIFO 唤醒顺序——这些都得由业务逻辑自己兜住。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











