因为 std::atomic 不支持阻塞等待,仅提供无锁读写,而信号量需“等待值大于0”,必须结合系统调度(如futex或condition_variable)或C++20的std::atomic_wait/notify;纯自旋浪费CPU,纯mutex丧失原子轻量性。

为什么不能直接用 std::atomic 实现完整信号量
因为 std::atomic 本身不提供「等待直到值大于 0」的能力——它只能做无锁读写,但阻塞等待必须配合系统调度(如 futex、condition variable)或自旋策略。纯原子变量 + 自旋在高竞争或长时间等待下会浪费 CPU,而直接用 std::mutex + std::condition_variable 又失去原子操作的轻量优势。
真正高性能的实现,是在原子操作基础上,仅在必要时才陷入内核等待。Linux 下典型做法是用 futex 系统调用;跨平台则常用 std::condition_variable + std::atomic 协作,但需避免虚假唤醒和 ABA 风险。
-
std::atomic<int></int>适合做计数器,但wait()/notify_one()不是它的成员函数——那是std::atomic_wait(C++20 起)的事 - C++17 没有
std::atomic_wait,强行轮询load(std::memory_order_acquire)是常见错误,尤其在低频信号场景下耗电严重 - 即使 C++20,
std::atomic_wait在 Windows 上依赖SleepConditionVariableSRW,不是所有平台都高效
C++20 原生方案:用 std::atomic_wait 和 std::atomic_notify_one
这是最接近“原子变量直连内核”的方式,无需额外锁对象,但要求编译器和标准库支持(GCC 11+、Clang 13+、MSVC 19.30+),且必须用 std::memory_order_relaxed 配合 wait/notify。
class semaphore {
std::atomic<long> count_;
public:
explicit semaphore(long initial = 0) : count_(initial) {}
<pre class="brush:php;toolbar:false;">void acquire() {
while (true) {
long exp = count_.load(std::memory_order_relaxed);
if (exp > 0 && count_.compare_exchange_weak(exp, exp - 1,
std::memory_order_acquire, std::memory_order_relaxed)) {
return;
}
// 等待值变化,避免忙等
std::atomic_wait(&count_, exp, std::memory_order_relaxed);
}
}
void release() {
long prev = count_.fetch_add(1, std::memory_order_release);
if (prev <p>};</p>
- 注意
std::atomic_wait的第三个参数是「期望值」,不是谓词;它只在值等于该期望时才可能休眠,否则立即返回 -
compare_exchange_weak必须用std::memory_order_acquire保证后续内存访问不被重排到 acquire 前 -
release()中判断prev 是关键:只有当释放前计数为负(说明有人 blocked),才 notify;否则 notify 是冗余开销
C++17 兼容方案:原子计数器 + 条件变量协作
如果目标环境不支持 C++20 的 std::atomic_wait,就退回到经典模式:用 std::atomic 管理计数,用 std::mutex + std::condition_variable 处理阻塞逻辑。性能损失主要来自 mutex 争用,但可通过分离「快速路径」缓解。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
class semaphore {
std::atomic<long> count_;
std::mutex mtx_;
std::condition_variable cv_;
<p>public:
explicit semaphore(long initial = 0) : count_(initial) {}</p>
<pre class="brush:php;toolbar:false;">void acquire() {
while (true) {
long exp = count_.load(std::memory_order_acquire);
if (exp > 0 && count_.compare_exchange_weak(exp, exp - 1,
std::memory_order_acq_rel, std::memory_order_acquire)) {
return;
}
// 进入慢路径:加锁、检查、等待
std::unique_lock<:mutex> lk(mtx_);
if (count_.load(std::memory_order_acquire) > 0) continue; // 避免虚假唤醒后重复检查
cv_.wait(lk, [this]{ return count_.load(std::memory_order_acquire) > 0; });
}
}
void release() {
long prev = count_.fetch_add(1, std::memory_order_acq_rel);
if (prev lk(mtx_);
cv_.notify_one();
}
}</:mutex>
};
- acquire 的 fast path 完全无锁;只有在计数为 0 时才进 mutex 区域,减少锁争用
- 条件变量的 predicate 必须再次检查
count_,因为wait可能被虚假唤醒 -
release()中的notify_one()放在锁内,是因为cv_.notify_one()要求 mutex 已 lock(某些实现不严格,但标准要求如此)
容易被忽略的边界问题:溢出、负值语义、构造时机
信号量不是简单加减器。实际使用中,acquire() 失败不该抛异常(否则破坏 RAII),而应支持超时或取消;但更隐蔽的问题在初始化和生命周期上。
- 初始值设为负数是合法的,意味着启动时就有 N 个线程在等,但
count_.store(-5)后立刻acquire()可能导致未定义行为(取决于 wait 实现是否允许负初始值) - 如果
semaphore对象在多线程环境下被析构,而仍有线程在std::atomic_wait或cv_.wait中,就会访问已释放内存 —— 必须确保所有 acquire 已返回,或用std::shared_ptr管理生命周期 -
acquire()和release()不对称:一个acquire()对应一个release(),但若中途异常退出没 release,会导致资源永久泄漏;建议封装成scoped_semaphoreRAII 类
真正的高性能不只看单次操作快慢,而在于是否经得起压测下的公平性、饥饿避免和内存安全。原子变量只是工具,机制设计才是关键。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










