不能直接用 std::atomic_int 实现信号量,因其 fetch_sub 无条件扣减可能使值变负,破坏资源计数语义;需配合 compare_exchange_weak 循环或 wait/notify 实现“检查后减一”的原子操作。

为什么不能直接用 std::atomic_int 实现信号量?
因为信号量核心是「等待直到值大于 0,然后原子性地减 1」——这需要 compare_exchange_weak 循环或 wait/notify 配合,而裸 std::atomic_int 的 fetch_sub 会无条件扣减,可能变成负数,失去资源计数语义。
常见错误是写成这样:
sem.fetch_sub(1); // 错!没检查是否够用,可能欠账
结果导致逻辑错误:本该阻塞的线程直接往下走,破坏同步契约。
用 std::atomic_int + 自旋等待实现简易信号量
适用于低竞争、短等待场景(如内部线程池任务分发),不依赖操作系统调度,避免系统调用开销。
- 初始化为非负整数,表示可用资源数
-
acquire():循环尝试compare_exchange_weak,仅当当前值 > 0 时才减 1 并退出 -
release():直接fetch_add(1),无需同步判断 - 注意:自旋期间应加入
std::this_thread::yield()或_mm_pause()(x86)缓解 CPU 占用
示例关键片段:
void acquire() {
int expected;
do {
expected = sem.load();
if (expected == 0) {
std::this_thread::yield(); // 避免空转吃满核
continue;
}
} while (!sem.compare_exchange_weak(expected, expected - 1));
}用 std::atomic_int + wait/notify_one 实现更省电的版本
C++20 起 std::atomic 支持 wait 和 notify_one,能挂起线程而非自旋,适合等待时间不可预期的场景。
立即学习“C++免费学习笔记(深入)”;
-
acquire()中先读值,若为 0 则sem.wait(0);成功后再次检查并compare_exchange_weak - 必须在
release()后调用sem.notify_one(),但注意:notify 可能唤醒多个等待者,需配合循环检查 - 每次
wait前要确保已读到最新值(load(memory_order_acquire)),否则可能错过通知 - 不要在
wait返回后直接减 1 —— 必须再用compare_exchange_weak确保原子性
典型结构:
void acquire() {
while (true) {
int curr = sem.load(std::memory_order_acquire);
if (curr > 0 && sem.compare_exchange_weak(curr, curr - 1,
std::memory_order_acq_rel)) {
return;
}
sem.wait(curr, std::memory_order_acquire);
}
}容易被忽略的内存序和异常安全细节
信号量操作不是“随便加个 memory_order_relaxed 就行”——它承担着跨线程的同步职责。
-
acquire()中的compare_exchange_weak至少要用memory_order_acq_rel,保证后续操作能看到之前release()写入的数据 -
release()的fetch_add应用memory_order_release,与acquire()的 acquire 语义配对 - 构造函数/析构函数里不要做复杂逻辑;尤其避免在析构中
notify_all—— 此时其他线程可能正wait,而原子变量已被销毁 - 如果
acquire()可能被中断(比如响应 cancel token),需额外封装超时或取消逻辑,标准原子不提供 timeout
真正用起来时,最常翻车的是内存序设错导致数据竞争,或者忘了在 wait 后二次校验值 —— 这会让两个线程同时认为自己拿到了资源。


















