std::atomic 不能直接模拟信号量,因其缺乏阻塞等待能力,轮询会导致 cpu 空转;需结合 std::condition_variable 或 futex 实现正确等待/唤醒,兼顾性能与可移植性。

为什么不能直接用 std::atomic 模拟信号量
原子变量本身不提供“等待”能力——std::atomic 支持 load/store/fetch_add 等操作,但没有阻塞语义。直接轮询 while (sem.load() == 0) {} 会空转 CPU,且无法响应唤醒通知,这不是信号量,只是自旋锁的劣化版。
真正可用的轻量级信号量,必须结合系统原语(如 futex)或标准库同步设施(如 std::condition_variable),而原子变量只负责核心计数和状态同步。
用 std::atomic<int></int> + std::condition_variable 实现最小可行信号量
这是最平衡的选择:原子变量管计数,条件变量管等待/唤醒,避免系统调用开销过大,又不牺牲正确性。
-
std::atomic<int></int>存储当前可用资源数,初始化为初始值(如1表示二元信号量) - 每次
wait()先原子递减;若结果 std::condition_variable::wait() 等待 - 每次
signal()原子递增;若递增前值 notify_one() - 注意:必须用
std::mutex保护条件变量的 wait 调用,但**不保护原子操作本身**——原子操作是 lock-free 的,加锁仅用于条件变量协议
示例关键逻辑:
void wait() {
int old = sem_.fetch_sub(1, std::memory_order_acquire);
if (old lk(mtx_);
cv_.wait(lk, [this] { return sem_.load(std::memory_order_acquire) > 0; });
}
}
sem_wait 和 futex 在 Linux 上的轻量路径怎么绕不开
Linux 的 sem_wait(POSIX 信号量)底层用 futex:计数 > 0 时纯用户态原子操作,无需进内核;计数 ≤ 0 才触发 futex 系统调用挂起线程。这是真正的“轻量级”标杆。
想在 C++ 里逼近这个行为,只能手写 futex 调用(通过 syscall(SYS_futex)),但代价是平台绑定、无异常安全、且 glibc 的 sem_t 已经做了足够好的封装。除非你在写高性能中间件或 runtime,否则不建议重复造轮子。
- 使用
sem_t时记得用sem_init(&s, 0, 1)初始化,第二个参数为0表示进程内共享 -
sem_wait可被信号中断,返回 -1 并设errno = EINTR,需重试 -
std::atomic+std::condition_variable版本不会被信号中断,行为更可控,但多一次用户态锁竞争
性能差异在哪?别只看“原子”二字
很多人以为“用了 std::atomic 就一定快”,其实不然。高频争用下,std::condition_variable::wait() 的内核态切换成本远高于一次 futex 唤醒;但低频场景下,两者的差距微乎其微,而 std::atomic + cv 版本更易调试、可移植、能与 std::thread 生命周期自然对齐。
- 避免在
wait()的 lambda 中捕获大对象或做耗时计算——条件判断必须快,否则阻塞期间还卡住 mutex - 不要用
memory_order_relaxed操作信号量计数:至少需要acquire(wait 读)和release(signal 写)来保证同步可见性 - 如果只需要二元状态(比如互斥),直接用
std::atomic_flag或std::mutex更合适,信号量不是万能替代品
真正难的从来不是写个原子计数器,而是确定等待逻辑是否该进内核、要不要支持中断、以及谁来负责 cleanup —— 这些决定了你到底需要 POSIX 信号量、C++20 的 std::counting_semaphore,还是自己封装的折中方案。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











