c++20起应使用std::counting_semaphore或std::binary_semaphore,c++11–17需基于std::mutex+std::condition_variable手动实现并严格保护计数器原子性。

直接说结论:C++20 起用 std::counting_semaphore 或 std::binary_semaphore,别自己手写;C++11–17 则必须基于 std::mutex + std::condition_variable 实现,且需严格保护计数器的原子性。
std::counting_semaphore 在 C++20 中怎么用
它不是“高级互斥锁”,而是独立的、无所有权的资源计数器。典型用途是控制并发数量(比如最多 4 个线程能进临界区),或做生产者-消费者配对信号。
-
std::counting_semaphore sem(5)表示初始有 5 个许可,sem.acquire()阻塞直到获得一个许可,sem.release()归还一个许可; - 注意:
acquire()可能抛std::system_error(如被中断),实际代码中应捕获; -
try_acquire()是非阻塞版本,返回bool,适合轮询或超时场景; - 模板参数
MaxCount是编译期上限(如),运行时计数器不会超过它,但越界调用release()会触发std::overflow_error; - 和
std::mutex不同,release()可由任意线程调用 —— 这是优势也是风险,必须靠业务逻辑保证释放不乱序、不重复。
C++11–17 如何安全实现计数信号量
标准库没提供,但不能简单用全局 int 加 std::mutex 就完事。常见错误是把 wait() 和 signal() 的临界区拆开,导致唤醒丢失或虚假唤醒未处理。
- 核心结构必须包含:一个
std::mutex、一个std::condition_variable、一个受保护的int计数器; -
wait()必须在while (count 循环中调用 <code>cv.wait(lock),不能用if—— 否则可能因虚假唤醒直接跳过等待; -
signal()必须在修改计数器后调用cv.notify_one()(或notify_all()),且 notify 必须在锁内或锁刚释放前完成,否则可能唤醒失败; - 避免用
std::this_thread::sleep_for模拟等待 —— 这不是同步,是竞态放大器; - 如果需要跨进程,C++ 标准库不支持,得切到 POSIX
sem_t或 WindowsCreateSemaphore。
binary_semaphore 和 mutex 的关键区别在哪
表面看都是“二选一”,但语义完全不同:一个管“许可”,一个管“所有权”。
-
std::binary_semaphore是std::counting_semaphore的别名,acquire()成功后,任何线程都能release(),哪怕不是 acquire 的那个线程; -
std::mutex要求lock()和unlock()必须成对出现在同一线程,否则行为未定义(多数实现直接 abort); - 这意味着
binary_semaphore可用于线程间“接力”通知(如 A 等待、B 唤醒),而mutex不能; - 但反过来,
mutex自带 RAII 支持(std::lock_guard),binary_semaphore没有自动管理机制,acquire()后忘记release()就会导致永久阻塞; - 性能上,
binary_semaphore在部分平台可纯用户态实现,mutex通常涉及内核态切换,但差异在现代系统中已不明显。
容易被忽略的边界问题
信号量不是万能胶,几个硬伤常被低估:
- 没有内置超时的
acquire()(C++20),想加 timeout 得自己封装try_acquire_for()并配合steady_clock; - 计数器溢出检查只在
release()时发生,如果多个线程反复 release 同一个信号量,可能 crash 而非静默失败; - 调试困难:信号量阻塞不会像 mutex 那样留下锁持有者线索,gdb / lldb 很难定位谁卡在了哪次
acquire(); - 与
std::jthread或异常传播结合时,若线程在acquire()中被request_stop()中断,需手动处理stop_token,标准信号量不感知停止请求。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











