因为std::atomic不提供阻塞等待能力,其操作均为无锁非阻塞;信号量需在资源不足时挂起线程,必须结合std::mutex与std::condition_variable实现正确等待唤醒。

为什么不能直接用 std::atomic<int></int> 实现信号量
因为原子变量本身不提供“等待”能力——std::atomic 的 load()、store()、fetch_add() 都是无锁但非阻塞的。信号量的核心语义是:当资源不足时,调用 wait() 必须挂起线程,直到其他线程调用 signal() 释放资源。单纯靠轮询 atomic.load() 不仅浪费 CPU,还可能因编译器/CPU 重排导致逻辑错误(比如漏检值变化)。
必须搭配条件变量才能正确阻塞等待
标准做法是用 std::atomic<int></int> 管理计数器,再用 std::mutex + std::condition_variable 实现等待唤醒。注意:原子变量只负责高效更新计数,阻塞逻辑完全交给条件变量,二者职责分离。
常见错误是把 wait() 写成:
while (counter.load() <p>这既耗资源,又无法响应中断或超时,也不符合 POSIX/Windows 信号量语义。</p><p>正确结构要点:</p>
-
counter用std::atomic<int></int>存储当前可用资源数,初始化为初始值 -
wait()中先原子递减;若结果 std::unique_lock 锁住互斥量,再通过cv.wait()挂起 -
signal()原子递增计数器,若递增前值 cv.notify_one() - 所有对
cv和mutex的访问必须严格受同一std::mutex保护
wait() 和 signal() 的原子操作顺序很关键
递减和递增操作必须是带内存序的原子操作,否则可能被乱序执行破坏逻辑。推荐统一使用 memory_order_seq_cst(默认),除非你明确需要性能优化并理解各内存序含义。
例如,wait() 中这行:
int old = counter.fetch_sub(1, std::memory_order_seq_cst);
确保“检查是否够用”和“扣减”是原子不可分的。如果用 memory_order_relaxed,可能在多核下看到旧值,导致重复扣减或漏唤醒。
同理,signal() 中:
int old = counter.fetch_add(1, std::memory_order_seq_cst);
必须保证该递增对所有线程立即可见,否则等待线程可能永远收不到通知。
一个最小可运行的 C++11 实现
以下代码去掉异常处理和调试断言,仅保留核心逻辑:
class Semaphore {
std::atomic<int> counter_;
std::mutex mtx_;
std::condition_variable cv_;
<p>public:
explicit Semaphore(int initial) : counter_(initial) {}</p>
<pre class="brush:php;toolbar:false;">void wait() {
int old = counter_.fetch_sub(1, std::memory_order_seq_cst);
if (old lock(mtx_);
cv_.wait(lock, [this] { return counter_.load(std::memory_order_acquire) > 0; });
}
}
void signal() {
int old = counter_.fetch_add(1, std::memory_order_seq_cst);
if (old <p>};</p>
注意:cv_.wait() 的谓词里用 load(std::memory_order_acquire) 是为了确保能读到最新值;signal() 中判断 old 而不是 <code>old ,是因为 <code>wait() 里已扣减一次,old == 0 表示刚好数量归零但无人等待。
这个实现支持多生产者/多消费者,但不支持中断等待或超时 —— 如需这些特性,得改用 cv.wait_for() 或平台原生信号量(如 sem_wait())。
最容易被忽略的是:wait() 返回后,计数器值可能仍 ≤ 0(比如多个线程同时被唤醒,但只有一个能真正获得资源),所以实际业务中不要依赖返回时的计数值做分支判断,而应把它当作纯同步原语使用。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











