Skip to content

std::atomic 与内存序 / volatile / 无锁编程基础

一、std::atomic 是什么

提供原子操作类型:对它的读改写(load/store/fetch_add/compare_exchange…)在硬件层面是不可分割的,不被打断。避免数据竞争。

cpp
std::atomic<int> c{0};
c.fetch_add(1);          // 原子 +1
c.store(10);
int v = c.load();
  • 常用整数类型(int/long/指针/bool)通常**无锁(lock-free)**实现。
  • 自定义类型若 is_trivially_copyable 且大小够,也能有原子操作。
  • 真实世界「多个线程同时 ++ 一个 int」必须用 atomic 或加锁,否则是数据竞争 UB。

二、内存序(memory order)——进阶必考

原子操作还涉及可见性与顺序std::memory_order 六档,常用三档:

内存序语义用途
memory_order_relaxed只保证该操作原子,不保证顺序独立计数器(如计数、统计)
memory_order_acquire(读)之后的读写在 acquire 之后可见锁/释放-获取配对
memory_order_release(写)之前的写对 acquire 之后可见同左
memory_order_acq_rel兼有(读改写)CAS 类
memory_order_seq_cst(默认)全序、最严格最易写对但较重

经典「释放-获取」配对(正确发布数据)

cpp
std::atomic<bool> ready{false};
int data = 0;

// 线程 A(写者)
data = 42;                                   // 普通写
ready.store(true, std::memory_order_release);// 发布:此前写对 acquire 可见

// 线程 B(读者)
while (!ready.load(std::memory_order_acquire)); // 读到 true
// 此时保证能看到 data==42(A 在 release 前写都被看到)

记忆:release 像"落锁",acquire 像"开锁"——开锁后能读到锁里保护的一切写。

为什么默认 seq_cst?

最直观的全序保证,不写错;代价是更强的同步可能略耗性能。多数应用用默认即可,仅热路径锁/自旋再调序。

三、CAS(compare_exchange / compare_exchange_strong)

无锁算法的基石:仅当当前值等于 expected 才更新为新值,否则把 expected 更新为当前值。

cpp
std::atomic<int> val{0};
// 循环 CAS 原子自增
int cur = val.load();
while (!val.compare_exchange_weak(cur, cur + 1)) {
    // cur 被更新为最新值,重试
}
  • compare_exchange_weak 可能虚假失败(x86 少用;ARM 常用 weak 配合循环)。
  • compare_exchange_strong 不虚假失败、无需循环重试(但更贵)。
  • 用 CAS 实现 无锁栈/队列/自旋锁(ABA 问题需注意,可加 tag/版本)。

四、volatile vs atomic(高频搞混题)

volatileatomic
作用阻止编译器优化访问(不缓存到寄存器),每次真实读内存提供原子性 + 可选内存序同步
线程安全❌ 不能避免数据竞争,不保证可见性序与原子✅ 专为多线程正确同步设计
跨线程不能当同步机制是的同步工具
用途硬件寄存器、信号、嵌入式(与外部异步改动)共享计数、标志、发布订阅

结论volatile 不能解决多线程并发写同一变量的竞争(它只是防止编译器把值缓存进寄存器,但仍可能发生读改写交错、且无序保证)。想让变量在多线程正确同步:用 std::atomic 或锁。很多面试者以为 volatile 能同步,是最大误区。

五、无锁 / lock-free 概念

  • lock-free:至少有一个线程总能推进(系统级不因某线程被调度阻塞而整体卡死)。
  • lock-free 不等于无等待 wait-free。
  • CAS 重试是 lock-free 的典型代表,需注意 ABA。
  • 常用工具:std::atomic_flag(真 bool,无锁自旋锁基础)。
cpp
// 用 atomic_flag 实现自旋锁
class SpinLock {
    std::atomic_flag flag = ATOMIC_FLAG_INIT;
public:
    void lock()     { while (flag.test_and_set(std::memory_order_acquire)); }
    void unlock()   { flag.clear(std::memory_order_release); }
};

追问:自旋锁 vs 互斥锁?

  • 自旋锁:忙等(不睡眠),适合临界区极短、不发生线程切换省钱;会烧 CPU、在单核/被抢占时可能死锁(持锁线程被调度跑走)。
  • mutex:让出 CPU 睡眠等待,适合持锁时间长;有上下文切换开销。

C++ 面试八股 · VitePress 版