c++oding="utf-8" ?>
不能直接用 std::atomic 实现信号量,因为原子操作不支持阻塞等待;信号量的 p 操作需在计数为 0 时挂起线程,而 atomic 仅提供无等待计数,必须配合 mutex 和 condition_variable 才能正确实现等待-唤醒语义。

为什么不能直接用 std::atomic<int></int> 实现信号量
直接对 std::atomic<int></int> 做 fetch_add 或 load/store 无法保证「等待-唤醒」语义:当计数器为 0 时,线程必须阻塞;而原子操作本身不提供阻塞能力。裸原子变量只能做无等待(wait-free)的计数,但信号量核心是「有等待」——即 P/V 操作中 P(wait)可能挂起线程。
常见错误是写成这样:
void wait() { while (counter.load()
<p>这会引发忙等(busy-wait),CPU 占用率飙升,且在多核下存在 ABA 风险和内存序漏洞。</p>
<p>真正可行的做法是:用原子变量管理计数,但阻塞逻辑交给系统原语(如 futex、condition_variable)——C++ 标准库没提供 futex,所以得靠 <code>std::condition_variable</code> + <code>std::mutex</code> 组合,或 C++20 的 <code>std::counting_semaphore</code>(底层仍依赖平台原语)。</p>
<h3>C++20 <code>std::counting_semaphore</code> 是最简方案</h3>
<p>如果你用的是 C++20 或更新标准,直接用标准库提供的 <code>std::counting_semaphore</code>,它已封装好原子计数 + 系统级等待,无需手写。</p>
-
std::counting_semaphore构造时指定最大值(注意:模板参数是编译期常量,不是运行时值) -
s.acquire()等价于 P 操作,会阻塞直到计数 > 0,然后原子减 1 -
s.release()等价于 V 操作,原子加 1,并唤醒等待线程(如有) - 不支持超时等待(
try_acquire_for等需自行包装)
示例:
std::counting_semaphore sem{3}; // 初始值 3<br>sem.acquire(); // 阻塞直到可获取,成功后计数变为 2
手写简易版:用 std::atomic<int></int> + std::condition_variable
若需兼容 C++17 或想控制细节(比如加超时、调试计数变化),可用原子变量配合条件变量。关键点在于:原子变量只管计数,阻塞/唤醒由条件变量负责,避免竞态。
典型陷阱:
- 忘记在
wait()中用while循环检查条件(spurious wakeup) - 在
notify_one()前未更新原子计数,导致唤醒后仍为 0 - 锁粒度太大:把整个 acquire/release 都锁住,抵消原子变量优势
推荐结构:
class semaphore {<br> std::atomic<int> count_{0};<br> mutable std::mutex mtx_;<br> std::condition_variable cv_;<br>public:<br> explicit semaphore(int init) : count_{init} {}<br> void acquire() const {<br> std::unique_lock lk{mtx_};<br> cv_.wait(lk, [&] { return count_.fetch_sub(1, std::memory_order_acquire) > 0; });<br> }<br> void release() const {<br> int prev = count_.fetch_add(1, std::memory_order_release);<br> if (prev }<br>};</int>
注意:acquire 中的 fetch_sub 放在 lambda 里是错的——它会在每次 spurious wakeup 时重复执行,导致计数错乱。正确做法是先检查再减,或改用两步:先 load 再 CAS。
性能敏感场景下要注意什么
纯原子信号量(如 Linux futex 封装)比 condition_variable 版本快一个数量级,因为避免了用户态锁和内核态切换开销。但 C++ 标准库不暴露 futex 接口,手写需平台相关代码(syscall(SYS_futex) on Linux)。
如果真要极致性能:
- 确认是否真的需要信号量——有时
std::atomic_flag自旋锁或std::latch/std::barrier更合适 - 避免在 hot path 上频繁调用
acquire/release:可批量操作(如一次 acquire 3 个资源)减少同步次数 - 注意
std::counting_semaphore在 libc++ 和 libstdc++ 中实现差异:前者用 futex(Linux),后者可能退化为 mutex+cv
最后提醒:信号量容易引发死锁或资源泄漏,尤其是跨异常边界未 release 时。生产环境优先考虑 RAII 封装(如 scoped_semaphore),而不是裸调用。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











