std::atomic本身不提供阻塞等待语义,仅保证原子读写;实现信号量需结合futex(linux)或waitonaddress(windows)等系统原语,否则轮询会耗尽cpu,且必须严格遵循检查-休眠-再检查模式并配对memory_order_acquire/release以避免唤醒丢失。

为什么不能直接用 std::atomic 实现信号量?
因为 std::atomic 本身不提供“等待直到值大于 0”这种阻塞语义——它只保证读写原子性,不带同步原语(如 futex 或条件等待)。直接轮询 load() 会吃满 CPU,而手写自旋+yield 又难兼顾公平性和唤醒及时性。
真正低开销的实现必须结合操作系统支持:Linux 下靠 futex 系统调用,Windows 下靠 WaitOnAddress 或 SlimReaderWriterLock 模拟。标准库没封装这个,得自己桥接。
Linux 上用 futex 实现零分配、无锁路径的信号量
核心思路是:用 std::atomic<int></int> 存计数器,正常情况下的 acquire() 和 release() 完全无系统调用;仅当计数器为 0 且需等待时,才陷入内核触发 futex_wait。
-
futex地址必须是对齐的int*,且生命周期需覆盖整个信号量对象(不能指向栈变量或临时对象) - 调用
futex_wait前必须先load()确认值仍为 0,否则可能错过唤醒(lost wakeup) - 唤醒方调用
futex_wake时,传入的地址和等待方一致,且无需加锁——futex内部通过地址哈希做等待队列管理
示例关键片段:
int counter_;
// ... 初始化为初始值
int acquire() {
while (true) {
int v = counter_.load(std::memory_order_acquire);
if (v > 0 && counter_.compare_exchange_weak(v, v - 1,
std::memory_order_acq_rel)) {
return 0;
}
if (v == 0) {
syscall(SYS_futex, &counter_, FUTEX_WAIT_PRIVATE, 0, nullptr, nullptr, 0);
}
}
}
Windows 上如何避免 CreateSemaphore 的句柄开销?
CreateSemaphore 每次创建都分配内核对象,频繁创建销毁极慢;更优路径是复用 WaitOnAddress(Win10 1809+)模拟用户态等待。
- 用
std::atomic<long></long>存计数,WaitOnAddress等待地址值变化,比事件/互斥体轻量得多 - 注意:该 API 要求等待地址对齐到指针大小(
sizeof(LONG)),且只能用于LONG类型 - 唤醒时调用
WakeByAddressAll或WakeByAddressSingle,后者更省,但需配合公平性逻辑(比如用队列序号控制唤醒顺序)
失败回退路径:若 WaitOnAddress 不可用(旧系统),再 fallback 到 InitializeCriticalSection + 条件变量组合,但务必缓存句柄,别每次 new。
跨平台封装时最易忽略的内存序与唤醒丢失问题
所有平台下,release() 中的计数器递增必须用 std::memory_order_release,而 acquire() 中的 load 必须用 std::memory_order_acquire——否则编译器或 CPU 可能重排指令,导致等待线程永远看不到新值。
更隐蔽的是唤醒丢失:如果唤醒操作发生在等待线程进入内核等待前的瞬间,futex 或 WaitOnAddress 就收不到信号。必须严格遵循“检查 → 条件成立则休眠 → 再检查”的模式(即 double-checked locking 变体),且休眠前不能有任何分支跳过检查。
另外,try_acquire() 接口必须返回 bool,且不能隐式触发等待;很多实现在这里误用了阻塞逻辑,导致性能拐点突变。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











