我如何把 C++ 原子变量(std::atomic)讲清楚:定义 → 编译器 → CPU/运行时

5 阅读4分钟

我在简历里写了“熟悉 atomic”,但我不希望把原子变量讲成一堆细节堆砌。更理想的状态是:面试时能在 1~2 分钟内把 atomic 的核心价值讲清楚,并且能顺着回答两个常见追问:

  1. atomic 是什么,解决什么问题
  2. 编译器如何处理 atomic,机器码和普通变量有什么区别
  3. 运行时执行到这些机器码时,CPU、cache、memory 里发生了什么

这篇文章就是我对这三块内容的整理。


1. atomic 是什么

原子变量(std::atomic<T>)可以概括为两层能力:

1.1 它是一种变量类型,提供“不可撕裂”的访问

atomic 的读写是原子的,不会出现“读到一半新、一半旧”的撕裂状态。对一个 atomic 变量的读取,结果只能是:

  • 旧值
  • 新值

不会是中间态。

1.2 它还能建立跨线程的可见性与先后关系

atomic 不只是“读写不撕裂”,更重要的是它提供了并发下的可见性顺序约束。这一点由 **memory order(内存序)**控制,核心作用包括:

  • 哪些操作不能被重排序
  • 一个线程的写什么时候对另一个线程可见
  • 用怎样的“同步边”把两段代码连起来(经典就是 release / acquire

2. 编译器层面:为什么 atomic 会被特殊对待

2.1 编译器为什么要特殊处理 atomic

普通变量如果被多个线程并发读写,会发生 data race,在 C++ 里这会导致 UB(未定义行为)

UB 的关键含义是:
编译器可以基于“单线程假设”做非常激进的优化,导致并发下的行为无法预测。

atomic 是语言层面的承诺:
即使并发访问,行为仍然定义良好。因此 atomic 本质上是一个跨线程通信点,编译器必须遵守内存序语义。

2.2 编译器保证三件事

  • 原子性(Atomicity)

    • 单次访问不可撕裂(只能读到旧值或新值)
    • RMW 操作不可被拆分(不能变成 load + op + store
  • 顺序(Ordering)

    • memory_order 限制编译器重排
    • 必要时生成更强的指令或屏障维持顺序语义
  • 可见性(Visibility)

    • 规定一个线程的写何时对另一个线程可见
    • 通过 release / acquire / seq_cst 等形成同步边

2.3 编译器对优化的限制

一句话总结:
编译器不能把 atomic 当普通变量“随便缓存、合并、删掉、乱重排”。

常见约束包括:

  • 不能长期缓存到寄存器
  • 不能把多次 atomic load 合并成一次
  • 不能把 atomic store 当“无用写”消掉
  • 不能把 atomic 与周围内存访问随意重排

2.4 生成机器码:和普通变量的区别

  • atomic load/store

    • 在某些架构上“看起来还是 mov/ldr/str”(尤其 x86)
    • 差别不一定是指令长相,而是:编译器不敢乱优化 + 必要时会加屏障/更强语义
  • atomic RMW / CAS(差异最明显)

    • 常用专门的原子指令或原子序列
    • x86:常见 lock 前缀相关指令
    • ARM:常见 ldxr/stxr(独占对)+ 必要的 barrier

2.5 编译器层总结

  • 普通变量:编译器只保证单线程语义,并发读写可能 UB
  • atomic:编译器按内存序把它当成同步点,限制优化,并在需要时生成原子指令/屏障或退化到库/锁来保证语义

3. CPU 与运行时层面:机器码执行时发生了什么

这一层我希望能讲清一件事:
atomic 在运行时并不是“魔法”,它落地在 CPU 的 cache/coherence 与 barrier 语义上。

3.1 执行与调度:OS 的参与点

  • OS 调度线程到不同 CPU core 运行,线程可能发生迁移
  • OS 不负责实现原子性,原子语义主要由 CPU 指令与硬件内存模型保证
  • OS 主要参与线程切换与阻塞机制,atomic 自旋属于用户态忙等

3.2 内存访问路径:CPU 实际如何读写

  • 指令在 core 上执行,数据访问先到寄存器与本核 cache hierarchy(L1/L2/L3)
  • store 通常先进入 store buffer,再逐步对外可见
  • load 优先命中本核 cache,不命中再向更高层级或 DRAM 获取
  • 共享变量所在的 cache line 是跨核竞争与同步的基本单位

4. 面试时的“可复述主线”

我用三句话把它串起来:

  1. atomic 在语言层保证并发访问定义良好,提供原子性与可见性/顺序语义。
  2. 编译器把 atomic 当作跨线程通信点,限制优化并按内存序生成相应的原子指令或屏障。
  3. 运行时这些指令落到 CPU 的 cache coherence、store buffer 与 barrier 上,通过对 cache line 的协调实现原子更新与发布-获取可见性。