std::counting_semaphore的初始值即最大资源数,构造后不可动态调整,必须重建对象才能变更容量;其计数可累积,而binary_semaphore仅限0/1状态且release超限时行为未定义。

std::counting_semaphore 的初始值就是最大资源数
它不提供运行时动态调整容量的接口,initial_count 在构造时就锁死了能同时通行的线程上限。比如 std::counting_semaphore{2} 表示最多 2 个线程能同时持有许可;设成 {0} 就是纯阻塞信号量,等同于二元信号量的“初始不可用”状态。
常见错误是误以为能像某些第三方库那样调用 set_capacity() 或类似方法扩容——C++20 标准里没有这个能力。资源数变更必须靠重建对象(注意:这会丢失当前所有等待线程的状态)。
- 初始化值必须是非负整数,负值触发编译期断言失败(
static_assert) - 模板参数
N是底层计数类型的最大安全位宽,不是资源数;实际资源数由构造参数决定 - 若资源数很大(如百万级),
std::counting_semaphore可能比更省内存,但要注意溢出风险
acquire() 和 release() 的语义与典型误用
acquire() 是阻塞式申请一个许可,release() 是归还一个许可。两者都保证原子性,但 release() 永远不会阻塞(即使当前计数已达最大值,标准允许它静默忽略或抛异常,实践中主流实现(libstdc++、libc++)选择忽略)。
最容易踩的坑是:在未成功 acquire() 前就调用 release(),或者重复 release() 导致计数超过初始值——这不违法,但会破坏你对“资源数”的建模逻辑。例如你用信号量保护 3 个数据库连接,却意外 release() 了 5 次,后续 acquire() 就可能拿到“虚高”的许可。
立即学习“C++免费学习笔记(深入)”;
- 务必配对使用:每个逻辑上的“进入临界区”对应一次
acquire(),每个“退出”对应一次release() - 推荐用 RAII 封装,比如自定义
semaphore_guard,避免异常绕过release() -
try_acquire()可做非阻塞探测,返回bool,适合超时或轮询场景
和 std::binary_semaphore 的关键区别在哪
std::binary_semaphore 是 std::counting_semaphore 的别名,但语义更严格:它的内部计数只允许 0 或 1,release() 在已为 1 时行为未定义(libstdc++ 直接 abort)。而 counting_semaphore 允许计数累积,只要不超过模板参数 N 所隐含的位宽上限(例如 最大支持 65535)。
所以不要因为“只需要互斥”就默认选 binary_semaphore——如果你的场景存在“先批量释放再统一获取”(比如生产者攒够 N 个任务才通知消费者),那只有 counting_semaphore 能自然表达。
- 性能上,
binary_semaphore在部分平台可能有更轻量的底层实现(如基于 futex 的单比特操作) - 可读性上,用
binary_semaphore能向协作者明确传达“这里只允许 0/1 状态” - 二者都不能跨进程共享,仅限同一进程内线程间同步
Windows 和 Linux 下的兼容性现实
libc++(Clang)和 libstdc++(GCC ≥11)均已实现 std::counting_semaphore,但依赖系统原语:libstdc++ 在 Linux 用 futex,在 Windows 上回退到 CRITICAL_SECTION + 自旋;libc++ 在 Windows 需要 Winelib 支持,否则编译失败。
如果你的目标平台包含老旧 Android(NDK r21 及更早)或某些嵌入式 STL,很可能压根没实现该类——此时别硬套,改用 std::mutex + std::condition_variable 手写计数逻辑更稳妥。
- 检查是否可用:#ifdef __cpp_lib_semaphore 判断语言特性宏
- 避免在信号处理函数中调用任何 semaphore 方法(非异步信号安全)
- 调试时注意:GDB 对 semaphore 内部状态通常不友好,建议用日志打点
acquire/release调用位置










