std::atomic 无法直接模拟信号量,因其缺乏阻塞等待语义和休眠/唤醒机制;正确实现需保证 release 中先 fetch_add 再 notify_one 以避免唤醒丢失;低争用时可用带退避的自旋优化性能。

为什么不能直接用 std::atomic 模拟信号量?
因为 std::atomic 本身不提供“等待直到值大于 0”这种阻塞语义——它只支持无锁读写和 CAS,没有内置的休眠/唤醒机制。硬用 while (count.load() 就是忙等,CPU 白耗,还可能被编译器优化掉(尤其没加 <code>memory_order_relaxed 或 memory_order_acquire 时)。
真正轻量级的实现必须结合系统原语(如 futex)或标准库同步设施(如 std::condition_variable),但又要避免 mutex 的重量开销。关键取舍点在于:是否允许短暂忙等 + 退避后交由内核调度。
用 std::atomic + std::condition_variable 实现最小可行信号量
这是最实用、跨平台、且不依赖 Linux futex 的方案。核心是用原子变量做快速路径(多数情况下无需锁),仅在资源不足时才进入条件变量等待。
-
count_是std::atomic<int></int>,初始值为最大许可数(如 1) -
mutex_只保护等待队列逻辑,不是每次acquire()都要 lock —— 先 CAS 尝试扣减,失败再进锁 -
cv_用于唤醒等待者,注意 notify_one() 足够,因为每次只放行一个
示例关键片段:
void acquire() {
int expected = count_.load();
while (expected > 0) {
if (count_.compare_exchange_weak(expected, expected - 1)) {
return; // 成功获取
}
// CAS 失败:值被改了,重试
}
// 到这里说明 count lk(mutex_);
cv_.wait(lk, [this] { return count_.load() > 0; });
count_.fetch_sub(1, std::memory_order_acq_rel);
}
release() 为什么必须区分 notify 和 atomic 操作顺序?
释放操作看似简单,但顺序错误会导致唤醒丢失:如果先 notify_one() 再 fetch_add(),而此时没有线程在 wait 中,通知就丢掉了;反之,如果先增加计数再 notify,但其他线程刚在 CAS 循环里读到旧值,就会错过这次 release。
正确做法是:
- 先
count_.fetch_add(1, std::memory_order_release) - 再
cv_.notify_one() - 确保 notify 发生在计数值已更新之后,且对等待线程可见(
memory_order_release与wait()内部的 acquire 构成同步)
注意:std::condition_variable::wait 内部自动使用 memory_order_acquire,所以无需额外 fence。
性能敏感场景下如何进一步减小开销?
当信号量争用极低(比如几乎总是有资源可用),可去掉条件变量,改用带退避的自旋 + yield:
- 用
std::this_thread::yield()替代锁和 cv,避免上下文切换成本 - 限制最大自旋次数(如 100 次),超时后再 fallback 到 full blocking path
- 对
acquire()加[[likely]]属性提示编译器预测成功路径 - 避免在
release()中调用任何可能分配内存的操作(如异常处理、日志),否则破坏“轻量”前提
真正的轻量不在于代码行数少,而在于:绝大多数路径不触发内核态切换、不分配堆内存、不抛异常、不依赖 TLS。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











