std::atomic不能直接当“开关”用,因忙等待浪费cpu且存在内存序可见性问题;应指定内存序、用wait避免轮询,并用compare_exchange_weak实现安全状态跃迁。

为什么 std::atomic<bool></bool> 不能直接当“开关”用
很多人一上来就写 std::atomic<bool> flag{false}</bool>,然后在线程里反复轮询 flag.load() 等待 true,以为这就叫“无锁”。但问题在于:这种忙等(busy-wait)既浪费 CPU,又没解决内存序引发的可见性陷阱。比如另一个线程改了 flag.store(true),当前线程可能因编译器重排或 CPU 缓存未同步而永远看不到变化——除非显式指定内存序。
实操建议:
- 默认用
std::memory_order_seq_cst最安全,但性能略低;对性能敏感路径可降级为std::memory_order_acquire(读) +std::memory_order_release(写),确保写入前所有操作对读线程可见 - 避免裸写
while(!flag) {},应改用flag.wait(false)(C++20 起支持,底层调用 futex 或类似机制,真正休眠而非忙等) - 注意:
std::atomic_flag是唯一保证无锁(lock-free)的原子类型,std::atomic<bool></bool>在某些平台可能被实现为加锁模拟,可用flag.is_lock_free()检查
用 std::atomic<int></int> 实现多状态标志位时的常见误用
有人想用一个 std::atomic<int> state{0}</int> 表示 INIT/READY/RUNNING/DONE 四种状态,然后用 state.store(2) 切换。这本身没问题,但容易踩两个坑:一是状态跳变不可控(比如从 0 直接到 3,跳过中间态),二是多个线程并发修改时丢失更新。
实操建议:
- 用
compare_exchange_weak做状态机跃迁,例如:int expected = INIT; while (!state.compare_exchange_weak(expected, READY)) { if (expected == DONE) break; // 已结束,不覆盖 expected = INIT; // 重试 } - 避免用
fetch_add或fetch_or修改状态值,除非你明确在做位运算标志(如std::atomic<uint32_t></uint32_t>存多个布尔位) - 若需原子地设置/清除某几位,用
fetch_or(1U 和 <code>fetch_and(~(1U ,但务必确认目标平台支持该宽度的无锁操作(<code>is_lock_free()要返回 true)
C++20 wait/notify 机制替代条件变量的适用边界
std::atomic::wait 看起来很美:不用互斥量、无锁、自动休眠唤醒。但它只适合“单生产者-单消费者”或“状态广播”类场景,不是万能替代品。
实操建议:
- 仅当等待条件是“某个原子变量等于某值”时才用
wait;如果要等“队列非空”或“多个变量组合条件”,仍得回退到std::condition_variable+ 互斥量 - 每次
notify_one()只唤醒一个等待线程,但notify_all()可能导致惊群(thundering herd),尤其在高并发下;若只需唤醒任意一个,优先用notify_one() - 注意:
wait不保证唤醒后值仍是预期值(可能被其他线程抢改),所以必须配合循环检查:while (flag.load() != true) { flag.wait(false); }
跨线程传递指针或对象地址时,std::atomic<t></t> 的陷阱
用 std::atomic<foo> ptr{nullptr}</foo> 传对象地址很常见,但极易出现悬垂指针:生产者线程 new 出对象、存入 ptr,消费者线程读到后使用,但生产者随后 delete 了它——此时消费者访问的是已释放内存。
实操建议:
- 单纯用
std::atomic<foo></foo>不解决生命周期管理问题;必须搭配智能指针(如std::atomic<:shared_ptr>></:shared_ptr>)或手动引用计数 - 若坚持裸指针,需确保对象生命周期长于所有可能的消费者线程(例如 static 对象、对象池预分配),并在所有消费者完成前禁止释放
-
std::atomic<foo>::is_lock_free()</foo>在多数平台返回 false(因为指针宽度 > 原子指令支持宽度),实际是加锁模拟,此时“无锁”名不副实
真正难的从来不是怎么写 store 或 load,而是厘清谁负责初始化、谁负责销毁、哪些操作必须成对出现、以及内存序如何与业务逻辑对齐。很多看似“无锁”的代码,其实只是把锁藏在了编译器或硬件的不可见路径里。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











