c++oding="utf-8" ?>
不能直接用 std::atomic 实现信号量,因其缺乏“等待直到非负”语义,fetch_sub() 等操作仅支持无等待或低效轮询,无法挂起线程;需结合 futex(linux)或 waitonaddress(windows)等系统原语,c++20 亦未提供原子等待标准支持。

为什么不能直接用 std::atomic<int></int> 实现信号量
因为原子整数本身不提供“等待直到非负”语义,std::atomic<int></int> 的 load() 和 fetch_sub() 只能做无等待(wait-free)或有等待但需手动轮询的逻辑——而轮询会空转耗 CPU,违背轻量级初衷。真正的信号量必须在资源不可用时挂起线程,而不是自旋。
所以关键不是“能不能用原子变量”,而是“怎么把原子操作和系统等待原语安全结合”。Linux 下最直接的是 futex,Windows 下对应 WaitOnAddress;C++20 标准没内置信号量原子等待,得靠平台支持。
- 仅靠
std::atomic<int></int>+while (v.load() 是错的:高竞争下性能崩塌,且无法响应中断或超时 -
std::counting_semaphore(C++20)是标准解,但它底层仍依赖系统原语,并非纯原子实现;若你禁用异常或需细粒度控制,就得自己封装 - 所有正确实现都必须满足:P 操作失败时阻塞,V 操作唤醒至少一个等待者,且唤醒/阻塞路径无竞态
Linux 下用 futex + std::atomic<int></int> 实现最小可行信号量
核心思路:用 std::atomic<int></int> 存计数,只在计数不足时才陷入内核调用 futex_wait;V 操作用 futex_wake 唤醒。注意 futex 要求地址对齐、值匹配,否则唤醒失效。
典型错误是把 atomic<int></int> 成员直接传给 futex_wait——它需要裸指针,且必须确保该内存不会被移动或析构。推荐用 alignas(4) std::atomic<int> m_count;</int> 配合 reinterpret_cast<int>( &m_count )</int>。
- 初始化必须为非负整数,且首次
futex_wait前要先load()确认值,避免唤醒丢失 - P 操作伪代码:
int old = m_count.fetch_sub(1, std::memory_order_acquire); if (old —— 注意传入的是 *旧值*,不是当前值 - V 操作必须用
std::memory_order_release更新计数,再futex_wake(&m_count, 1);若用relaxed,可能因重排导致唤醒早于计数更新,造成虚假唤醒或漏唤醒
C++20 std::counting_semaphore 的实际开销与替代场景
它确实是轻量级的,但“轻量”取决于实现:libstdc++(GCC)和 libc++(Clang)都基于 futex 或等价机制,无锁路径走原子操作,阻塞路径进内核。它的构造/析构零开销,acquire() 在有资源时就是一次 fetch_sub,比手写更可靠。
但如果你需要:带超时的 try_acquire_for、可取消的等待、或与现有 eventfd/fd 体系集成,std::counting_semaphore 就不够用了——它不暴露底层句柄,也没回调机制。
- 不要为了“看起来更底层”而放弃
std::counting_semaphore:它经过充分测试,处理了spurious wakeup、信号中断、多线程唤醒公平性等边界 - 若目标平台不支持 C++20(如旧嵌入式 STL),才考虑手写;否则优先用标准库,出问题也容易定位
- 性能差异通常只在极端高争用(>100k ops/sec)且频繁阻塞时显现,日常场景几乎无差别
Windows 下等效实现的关键差异点
Windows 没有 futex,但 WaitOnAddress(Win10 1607+)语义接近:等待某地址值等于预期值。它要求地址 8 字节对齐(alignas(8)),且只能用于 volatile LONG64* 或 volatile void* 类型。
这意味着你不能直接用 std::atomic<long long></long>,得配合 std::atomic_ref(C++20)或手动类型转换;且 WaitOnAddress 不支持超时精度低于 1ms,WakeByAddressSingle 唤醒也不保证 FIFO。
- 必须检查
IsWindowsVersionOrGreater(10, 0, 14393),否则回退到CreateSemaphore—— 后者是内核对象,开销明显更高 -
WaitOnAddress返回后,仍需重新load()判断是否真可用,因为存在虚假唤醒 - 跨平台封装时,别试图抽象出统一 API:Linux
futex和 WindowsWaitOnAddress的内存序约束、对齐要求、错误码完全不同,硬统一只会引入隐蔽 bug
真正难的不是原子操作本身,而是让等待/唤醒与计数值变更严格同步——哪怕一行内存序写错,就可能导致永久阻塞或资源泄露。平台原语只是工具,语义正确才是瓶颈。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











