编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • 进程地址空间布局
      • 对象内存布局原理
      • 引用与指针本质
      • this指针与成员函数
      • 虚函数表深度剖析
      • 多重继承内存模型
      • 内存对齐与缓存行
      • 内存分配器演进史
      • 五大值类别详解
      • 右值引用与移动语义
      • 完美转发与引用折叠
      • 类型推导三大规则
      • 类型转换与隐式构造
      • const与volatile真相
      • RTTI与dynamic_cast
      • 类型擦除技术原理
      • 模板实例化机制
      • 模板特化与偏特化
      • SFINAE与enable_if
      • 可变参数模板原理
      • constexpr编译期计算
      • Concepts深度剖析
      • 元编程模板技巧
      • Modules模块化设计
      • RAII的设计哲学
      • 对象构造与析构
      • 拷贝与移动控制
      • unique_ptr原理剖析
      • shared_ptr底层剖析
      • weak_ptr与this增强
      • 五种存储期管理
      • vector扩容真相
      • deque分段连续设计
      • list与forward_list
      • 关联容器红黑树
      • 哈希容器深度剖析
      • 迭代器五大类别
      • STL算法设计哲学
      • Allocator分配器机制
      • C++内存模型基石
      • 六大内存序详解
      • atomic原子操作原理
        • 1. 案例引入
          • 1.1 无锁栈的 ABA 幽灵
          • 1.2 atomic 的编译错误
          • 1.3 七个待解疑问
        • 2. 架构概览
          • 2.1 atomic 的三层实现模型
          • 2.2 为何这么切
        • 3. atomic 的底层实现原理
          • 3.1 x86 上的 LOCK 前缀——总线锁与缓存锁
          • 3.2 ARM 上的 LDREX/STREX——乐观锁
          • 3.3 编译器屏障与 CPU 屏障的组合
          • 3.4 sizeof(atomic) 什么时候等于 sizeof(T)
          • 3.5 原子操作的三层正确性保证
        • 4. lock-free 与 wait-free 的精确区别
          • 4.1 定义与区别
          • 4.2 islockfree() 与 isalwayslock_free 的差异
          • 4.3 各平台各类型的 lock-free 状态表
          • 4.4 当 atomic 不是 lock-free 时发生了什么
        • 5. CAS 与 ABA 问题
          • 5.1 compareexchangestrong vs compareexchangeweak
          • 5.2 ABA 问题的精确复现
          • 5.3 三种解决方案的对比
          • 5.4 双字 CAS 的使用场景
        • 6. atomic 对 T 的严格要求
          • 6.1 trivially copyable 是硬门槛
          • 6.2 为什么 string 不能 atomic
          • 6.3 平台大小的上限
          • 6.4 isalwayslock_free 的编译期判断
        • 7. atomic_ref —— 给非 atomic 变量套上原子外壳
          • 7.1 atomic_ref 的设计动机
          • 7.2 使用约束与陷阱
          • 7.3 与 atomic 的对比
        • 8. 常用原子操作的全览与选型
          • 8.1 load / store / exchange / fetch_add / CAS 的完整目录
          • 8.2 操作选型的决策树
          • 8.3 原子操作与 volatile 的本质区别
        • 9. 汇编全景与性能剖析
          • 9.1 各操作的 x86/ARM 指令对照
          • 9.2 benchmark——单核 vs 多核争抢的开销
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 原子操作的选型心智模型
          • 10.3 设计哲学回扣
          • 10.4 速查表合集
      • mutex与条件变量
      • thread与jthread机制
      • 异步编程future家族
      • 无锁数据结构设计
      • 协程coroutine原理
      • 翻译单元与预处理
      • 编译期符号生成
      • 链接器工作原理
      • ODR规则与陷阱
      • 动态库与符号可见性
      • C++ ABI兼容性
      • LTO与PGO优化
      • 异常机制底层原理
      • Ranges革命与管道
      • format与print体系
      • UB未定义行为图鉴
      • C++设计哲学回望
      • 写作模板
    • 开发技巧

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Cpp入门到精通
  • 专栏博客
杨充
2026-06-06
目录

atomic原子操作原理

# 42.atomic原子操作原理

# 目录介绍

  • 1. 案例引入
    • 1.1 无锁栈的 ABA 幽灵
    • 1.2 atomic<string> 的编译错误
    • 1.3 七个待解疑问
  • 2. 架构概览
    • 2.1 atomic 的三层实现模型
    • 2.2 为何这么切
  • 3. atomic 的底层实现原理
    • 3.1 x86 上的 LOCK 前缀——总线锁与缓存锁
    • 3.2 ARM 上的 LDREX/STREX——乐观锁
    • 3.3 编译器屏障与 CPU 屏障的组合
    • 3.4 sizeof(atomic<T>) 什么时候等于 sizeof(T)
  • 4. lock-free 与 wait-free 的精确区别
    • 4.1 定义与区别
    • 4.2 is_lock_free() 与 is_always_lock_free 的差异
    • 4.3 各平台各类型的 lock-free 状态表
    • 4.4 当 atomic 不是 lock-free 时发生了什么
  • 5. CAS 与 ABA 问题
    • 5.1 compare_exchange_strong vs compare_exchange_weak
    • 5.2 ABA 问题的精确复现
    • 5.3 三种解决方案的对比
    • 5.4 双字 CAS 的使用场景
  • 6. atomic<T> 对 T 的严格要求
    • 6.1 trivially copyable 是硬门槛
    • 6.2 为什么 string 不能 atomic
    • 6.3 平台大小的上限
    • 6.4 is_always_lock_free 的编译期判断
  • 7. atomic_ref —— 给非 atomic 变量套上原子外壳
    • 7.1 atomic_ref 的设计动机
    • 7.2 使用约束与陷阱
    • 7.3 与 atomic 的对比
  • 8. 常用原子操作的全览与选型
    • 8.1 load / store / exchange / fetch_add / CAS 的完整目录
    • 8.2 操作选型的决策树
    • 8.3 原子操作与 volatile 的本质区别
  • 9. 汇编全景与性能剖析
    • 9.1 各操作的 x86/ARM 指令对照
    • 9.2 benchmark——单核 vs 多核争抢的开销
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 原子操作的选型心智模型
    • 10.3 设计哲学回扣
    • 10.4 速查表合集

# 1. 案例引入

# 1.1 无锁栈的 ABA 幽灵

某高频交易系统的无锁栈——用 CAS 实现 push/pop。跑了一个月后出现订单重复处理——同一个订单被两个工作线程同时弹出:

// ====== 事故代码 V1:无锁栈的 ABA 问题 ======
struct Node { int value; Node* next; };
std::atomic<Node*> head = nullptr;

void push(int v) {
    Node* n = new Node{v, nullptr};
    do { n->next = head.load(); }
    while (!head.compare_exchange_weak(n->next, n));
}

int pop() {
    Node* n;
    do { n = head.load(); }
    while (n && !head.compare_exchange_weak(n, n->next));
    int val = n->value;
    delete n;                         // ① 释放节点
    return val;
}
// pop 的竞态时序——ABA:
//   时刻 T1:线程 1 读到 head=A
//   时刻 T2:线程 2 弹出 A → delete A → push(B) → push(C) → C.next = 以及 B
//   时刻 T3:线程 2 把 A 的地址分配给新的 Node —— A 的地址被复用!
//   时刻 T4:线程 1 的 CAS head==A → 成功!但 A 现在是另一个对象了
//   head = A(新) → A.next = B → 原来 head=C → C 丢失了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

根因:CAS 只比较地址值——地址 A 在 T1 和 T4 之间经历了「释放→重新分配」——地址值相同但对象变了。CAS 成功但语义已错。

# 1.2 atomic 的编译错误

同一个团队试图把 std::string 放进 std::atomic:

// ====== 事故代码 V2:atomic 不接受 string ======
std::atomic<std::string> name("config");  // ❌ 编译错误!

// GCC 输出:
// error: static assertion failed: std::atomic requires a trivially copyable type
1
2
3
4
5

std::string 不是 trivially copyable——它内部有堆分配的指针。std::atomic 要求类型可以用一条 CPU 指令(如 lock cmpxchg)原子地读写——std::string 不够简单。

# 1.3 七个待解疑问

① std::atomic 在 x86 和 ARM 上分别怎么实现? LOCK 前缀到底锁了什么?      → 第 3 章
② lock-free 和 wait-free 有什么区别? 怎么判断一个 atomic 是不是 lock-free? → 第 4 章
③ CAS 是什么? strong vs weak 有什么区别? ABA 问题怎么解决?               → 第 5 章
④ atomic&lt;T> 对 T 有什么要求? 为什么 string/vector 不行?                  → 第 6 章
⑤ atomic_ref 是什么? 给已有的非 atomic 变量加原子外壳?                    → 第 7 章
⑥ 原子操作有哪几种? 什么时候用 store/load、什么时候用 CAS?                 → 第 8 章
⑦ atomic 和 volatile 有什么区别? volatile 是原子的吗?                     → 第 8.3 / 第 10 章
1
2
3
4
5
6
7

# 2. 架构概览

# 2.1 atomic 的三层实现模型

┌────────────────────────────────────────────┐
│            std::atomic 实现架构             │
├────────────────────────────────────────────┤
│  ① 类型层:trivially copyable 约束         │
│     → sizeof(T) ≤ 平台最大值才可能 lock-free│
├────────────────────────────────────────────┤
│  ② 指令层:x86(LOCK前缀) / ARM(ldrex/strex)│
│     → 单条原子指令 or LL/SC 循环            │
├────────────────────────────────────────────┤
│  ③ 内存序层:memory_order 屏障              │
│     → 编译器屏障 + CPU fence               │
└────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

# 2.2 为何这么切

疑惑:为什么需要三层——不能直接一条 lock cmpxchg 搞定吗?

论证:

  1. 类型层是编译期的安全网——如果 T 不是 trivially copyable,memcpy(T) 不是安全操作(拷贝构造有副作用)。atomic<T> 必须能直接用 lock cmpxchg 复制 T 的字节——不需要调构造函数。
  2. 指令层是硬件依赖的最小化——x86 用 LOCK 前缀(总线锁或缓存锁),ARM 用 LDREX/STREX 的乐观并发。C++ 标准把这些差异封装在 std::atomic 的单一接口下。
  3. 内存序层是正确性的主角——即使有了原子指令,没有正确的 memory_order,并发操作仍然可能错误(第 41 篇)。

结论:atomic 不是「一条汇编指令」——是三层协作:类型系统过滤不安全的使用、硬件提供原子性、内存序提供可见性。 三层缺一不可。


# 3. atomic 的底层实现原理

# 3.1 x86 上的 LOCK 前缀——总线锁与缓存锁

; fetch_add(3, relaxed) → 
lock xadd [rax], 3          ; 原子地 *rax += 3
1
2

LOCK 前缀的两种模式:

① 总线锁(Bus Lock)——Intel 486/Pentium 时代
   LOCK# 信号拉低——物理总线被独占
   → 所有的内存访问被暂停——包括其他核和 DMA
   → 极其昂贵——整个系统暂停

② 缓存锁(Cache Lock)——现代 CPU(P6 及以后)
   如果操作数在 cache line 内——只锁该 cache line
   → 通过 MESI 协议:
     1. 发出 RFO(Read For Ownership)→ 独占该 cache line
     2. 执行原子操作
     3. 其他核看到该 cache line 为 M 状态——等待 ACK
   → 只影响一个 cache line——不阻塞其他核的内存访问
1
2
3
4
5
6
7
8
9
10
11
12

关键优化:现代 CPU 对同一 cache line 内的对齐访问使用缓存锁——快 10-50× 于总线锁。

# 3.2 ARM 上的 LDREX/STREX——乐观锁

ARM 没有 LOCK 前缀——用加载独占/存储独占对(Load-Linked / Store-Conditional):

; ARMv8 的 fetch_add:
.Lretry:
    ldaxr  w0, [x1]          ; ① Load-Exclusive-Acquire——读且标记独占
    add    w0, w0, #3         ; ② 本地加 3
    stlxr  w2, w0, [x1]       ; ③ Store-Exclusive-Release——写且检查独占标记
    cbnz   w2, .Lretry        ; ④ 如果 w2≠0 → 独占被打破 → 重试
1
2
3
4
5
6

LL/SC 的乐观并发语义:

ldaxr 标记地址 [x1] 为「独占监控」
stlxr 检查:
  ├─ 独占标记还在 → 写成功, 返回 0
  └─ 独占标记被破坏 → 写失败, 返回 1(其他核在 ldaxr 和 stlxr 之间访问了 [x1])
1
2
3
4

ARMv8.1 新增了原子指令(如 ldadd)——不再需要 LL/SC 循环——性能更高。

# 3.3 编译器屏障与 CPU 屏障的组合

// acquire load = 编译器屏障 + CPU acquire fence
// 编译器层面:asm volatile("" ::: "memory") —— 阻止编译器重排
// CPU 层面:x86 的 mov 自带 acquire ——不需要额外 fence

// release store
// CPU 层面:ARM dmb ish; str ——需要显式屏障
1
2
3
4
5
6

# 3.4 sizeof(atomic) 什么时候等于 sizeof(T)

static_assert(sizeof(std::atomic<int>)   == sizeof(int));         // ✅
static_assert(sizeof(std::atomic<void*>) == sizeof(void*));       // ✅
// 这些类型 <= 8 字节 —— 一条 lock cmpxchg 搞定
1
2
3

疑惑:为什么 atomic<T> 有时候比 T 大?内部的隐藏锁在哪?

论证——两种实现模式:

模式 A:lock-free(T ≤ 16 字节,且平台支持)
  atomic&lt;T> 内部只有一个 T 成员——sizeof 完全等于 T
  x86: lock cmpxchg16b 原子操作

模式 B:有锁(T > 16 字节 或 平台不支持)
  atomic&lt;T> 内部有 1 个 T + 1 个 mutex(或 spinlock)
  sizeof(atomic&lt;T>) = sizeof(T) + sizeof(mutex) ≈ sizeof(T) + 40
  每次 load/store = lock(mutex) + 读写 + unlock(mutex)
1
2
3
4
5
6
7
8

关键规则:如果 T 的大小 > 16 字节(多数平台)——atomic<T> 内部必然有隐藏的互斥锁——sizeof 会增加。

# 3.5 原子操作的三层正确性保证

层级 1:编译器层(volatile 替代品——不是!)
  编译器不能把 atomic load 优化掉(即使值「已知」)
  编译器不能把连续的 atomic store 合并成一个

层级 2:CPU 层(LOCK 前缀 / LL-SC)
  保证操作在硬件层面的不可分割性

层级 3:缓存一致性层(MESI)
  保证多核看到的内存是一致的——没有「stale cache」
1
2
3
4
5
6
7
8
9

# 4. lock-free 与 wait-free 的精确区别

# 4.1 定义与区别

属性 lock-free wait-free
定义 至少一个线程在有限步完成 每个线程在有限步完成
饥饿 可能——CAS 循环可能被其他线程反复干扰 不可能——每个线程有操作上限
实现复杂度 中等 高
C++ 标准支持 ✅ is_lock_free() ❌ 无标准检测

# 4.2 is_lock_free() 与 is_always_lock_free 的差异

// is_lock_free() —— 运行时检测(因为某些平台的某些类型在运行时才知道)
std::atomic<int> x;
bool b = x.is_lock_free();                  // 运行时

// is_always_lock_free —— 编译期常量(C++17)
constexpr bool always = std::atomic<int>::is_always_lock_free; // ✅ true
1
2
3
4
5
6

# 4.3 各平台各类型的 lock-free 状态表

类型 x86 ARMv7 ARMv8 ARMv8.1+
int ✅ ✅ ✅ ✅
void* ✅ ✅ ✅ ✅
double ✅ ✅ (8B) ✅ ✅
pair<int,int> (8B) ✅ ✅ ✅ ✅
pair<int,int> (16B) ✅ (cmpxchg16b) ❌ ✅ (LSE) ✅

# 4.4 当 atomic 不是 lock-free 时发生了什么

std::atomic<BigStruct> b;  // 内部有隐式的互斥锁

// fetch_add 等价于:
BigStruct atomic_fetch_add(std::atomic<BigStruct>* a, BigStruct v) {
    std::lock_guard<std::mutex> g(a->__internal_lock);
    BigStruct old = a->__value;
    a->__value += v;
    return old;
}
1
2
3
4
5
6
7
8
9

信号处理函数中用这种 atomic = 死锁——因为同一线程无法重新获取已经持有的互斥锁。


# 5. CAS 与 ABA 问题

# 5.1 compare_exchange_strong vs compare_exchange_weak

// strong —— 只有值不匹配时才失败
while (!head.compare_exchange_strong(old, new)) { old = head.load(); }

// weak —— 可能「伪失败」(spurious failure——即使值匹配也返回 false)
// 原因:ARM 的 LDREX/STREX 可能因中断被打破
// weak 在循环中通常配合 load 使用——伪失败多一次重试,但省一次 compare
while (!head.compare_exchange_weak(old, new)) {}
// old 在失败后自动更新为最新值——不需要重新 load
1
2
3
4
5
6
7
8

性能:weak 在 x86 和 strong 等价(lock cmpxchg 没有伪失败)。ARM 上 weak 少一条 cmp+mov——在 CAS 循环中更优。

# 5.2 ABA 问题的精确复现

案例 1.1 的完整时序:

初始状态:head → A(1) → B(2) → C(3)

线程 1: pop()                    | 线程 2: pop() + push()
─────────────────────────────────|─────────────────────────────────
n = head → A                     | pop → A → delete A
CAS(head,A,A.next) → 暂停        | pop → B → delete B
                                 | push(42) → Node D{42,next=C}
                                 |   分配器复用 A 的地址!→ D 在 A 的地址
                                 | push(99) → Node E{99,next=D}
现在 head → E(99) → D(42) → C(3)
                                 |
CAS(head,A,A.next)               |
  → read head → E                 |
  → 比较 head==A → false → 重试  |   ← 还好!因为 head 不是 A 了
  → 但如果是更精确的时序——也许是:
CAS(head,A,A.next)
  → head 此刻恰好是 D(A的地址复用)
  → head==A → true (地址匹配!) → CAS 成功
  → head = A.next = B → 但 B 已经 delete 了!
  → E 和 C 丢失——head 指向已释放内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 5.3 三种解决方案的对比

方案 原理 代价 实现
双宽 CAS CAS 同时检查指针+tag 需要 128bit CAS (x86 cmpxchg16b) 指针高位存 tag
引用计数 释放前检查是否有人持有引用 每次读写都要原子增减 RCU / hazard pointer
内存回收 延迟释放——等 safe epoch 内存占用更高 epoch-based reclamation

# 5.4 双字 CAS 的使用场景

struct TaggedPtr { Node* ptr; uint64_t tag; };  // 16 字节
std::atomic<TaggedPtr> head;  // cmpxchg16b 保证 lock-free
// tag 解决 ABA——每次修改 tag+1——即使地址复用,tag 不同 → CAS 失败
1
2
3

# 6. atomic 对 T 的严格要求

# 6.1 trivially copyable 是硬门槛

// ✅ trivially copyable:
int, void*, double, pair<int,int>, struct { int a; double b; }

// ❌ 不是 trivially copyable:
std::string     // 有堆指针、非平凡析构
std::vector     // 同上
std::map        // 同上
1
2
3
4
5
6
7

标准条款:[atomics.types.generic]/1——T 必须是 trivially copyable。

# 6.2 为什么 string 不能 atomic

std::atomic<std::string> 不能工作——因为 string 的拷贝涉及堆分配。lock cmpxchg 只能复制 16 字节的原始位——如果 string 有 SSO 缓冲区在对象内且 ≤16 字节——在特定平台上技术上可能工作——但标准不保证。依赖这种未定义行为是自找麻烦。

# 6.3 平台大小的上限

平台 最大 lock-free 大小 相关指令
x86-64 16 字节 lock cmpxchg16b
ARMv8.0 8 字节 ldxp/stxp (多字 LL/SC)
ARMv8.1 16 字节 caspl (LSE 扩展)
x86 (32位) 8 字节 lock cmpxchg8b

# 6.4 is_always_lock_free 的编译期判断

template <typename T>
void safe_atomic_use() {
    if constexpr (std::atomic<T>::is_always_lock_free) {
        // 可以安全地在信号处理函数中使用
        // 可以安全地用于共享内存
    } else {
        // 不能用于信号处理函数(内部有互斥锁)
        // 可以用,但有锁的开销
    }
}
1
2
3
4
5
6
7
8
9
10

# 7. atomic_ref —— 给非 atomic 变量套上原子外壳

# 7.1 atomic_ref 的设计动机

C++20 引入 std::atomic_ref<T>——包装已有的非原子对象,提供原子操作:

int shared_data = 0;  // 普通变量——不是 atomic

void thread_fn() {
    std::atomic_ref<int> ref(shared_data);  // 临时套上原子外壳
    ref.fetch_add(1);                       // 原子的!
} // ref 析构——shared_data 恢复为普通变量
1
2
3
4
5
6

# 7.2 使用约束与陷阱

  • 对象必须对齐到 required_alignment——通常是 sizeof(T)
  • 同一对象的所有 atomic_ref 访问必须是原子的——不能混用普通访问和 atomic_ref 访问
  • atomic_ref 本身不是线程安全的——不要在多个线程中同时修改同一个 atomic_ref 对象

# 7.3 与 atomic 的对比

维度 atomic atomic_ref
对象所有权 拥有 T 引用已有 T
内存布局 可能和 T 不同 T 保持原样
需要修改原类型 ✅ ❌

# 8. 常用原子操作的全览与选型

# 8.1 load / store / exchange / fetch_add / CAS 的完整目录

操作 语义 返回 典型用途
load() 原子读 当前值 读标志位
store(v) 原子写 — 写标志位
exchange(v) 原子交换 旧值 获取+设新值
fetch_add(v) 原子加 旧值 计数器、索引
fetch_sub(v) 原子减 旧值 引用计数
compare_exchange_weak/strong(exp, v) CAS bool+exp更新 无锁结构

# 8.2 操作选型的决策树

需要做什么?
├─ 只读当前值          → load()
├─ 只写新值 (不关心旧)  → store()
├─ 需要旧值来做决策     → exchange() 或 CAS
├─ 原子加减            → fetch_add / fetch_sub
├─ 复杂的条件更新       → CAS (weak in loop)
│   └─ 如: if (old == X) set to Y
└─ 需要同时更新两个变量  → 考虑双宽CAS 或 重新设计
1
2
3
4
5
6
7
8

# 8.3 原子操作与 volatile 的本质区别

volatile int x;                // ❌ 不是原子的!
x++;                           // 多线程下 data race!

std::atomic<int> x;            // ✅ 原子操作
x.fetch_add(1);                // 线程安全

// volatile 的含义:防止编译器优化掉读写(如 MMIO)
// atomic 的含义:多线程安全 + 内存序保证
// 两者完全不相关——不要混用
1
2
3
4
5
6
7
8
9

# 9. 汇编全景与性能剖析

# 9.1 各操作的 x86/ARM 指令对照

操作 x86 ARMv8.0 ARMv8.1+
load(relaxed) mov ldr ldr
store(relaxed) mov str str
exchange(seq_cst) xchg swpal swpal
fetch_add(relaxed) lock xadd LL/SC loop ldadd
CAS lock cmpxchg LL/SC loop casal

# 9.2 benchmark——单核 vs 多核争抢的开销

100 万次 fetch_add:

场景 x86 time ARMv8 time 说明
单核(无竞争) 14 ms 18 ms 只锁缓存行——快
2 核争抢 32 ms 48 ms 缓存行在两个核间跳动
8 核争抢 120 ms 210 ms 缓存行在 8 个核间连续跳(ping-pong)

# 10. 综合案例串讲

# 10.1 案例真相揭晓

# 疑问 答案
① x86/ARM 实现? 第 3 章:x86 LOCK 前缀(缓存锁/总线锁),ARM LDREX/STREX 乐观锁
② lock-free vs wait-free? 第 4 章:lock-free 有人最终完成、wait-free 每个人都有限步完成
③ CAS/ABA? 第 5 章:ABA 地址复用→弱于 strong→双宽 CAS/引用计数/延迟释放解决
④ T 的要求? 第 6 章:trivially copyable、大小≤平台上限、对齐要求
⑤ atomic_ref? 第 7 章:非 atomic 变量的临时原子外壳——不能混用普通访问
⑥ 操作选型? 第 8 章:load/store→基本/CAS→复杂条件/exchange→获取
⑦ vs volatile? 第 8.3:volatile 不是原子的——防止编译器优化≠多线程安全

案例①修复(ABA):用双宽 CAS(指针+tag)——每次修改 tag+1——地址复用被 tag 检测到。

案例②修复(atomic):用 std::atomic<std::string*> 或者自设计 trivially copyable 的字符串替代品(如固定 size 的 char 数组)。

# 10.2 原子操作的选型心智模型

三条原则:

  1. 类型先决:T 必须 trivially copyable,且 sizeof(T) ≤ 平台上限
  2. 操作选序:只读→load,只写→store,加减→fetch,条件→CAS
  3. 内存序后定:先 seq_cst 写对,再降级为 acquire/release

# 10.3 设计哲学回扣

哲学 1:原子性只是一半——可见性是另一半

std::atomic 保证了「读写的不可分割」。但如果没有正确的 memory_order——隔壁线程可能永远看不到你的修改。原子性 + 内存序 = 完整的线程安全。只写 atomic 不管 memory_order = 只做了一半的工作。

哲学 2:lock-free 不是免费的——它在缓存一致性协议上收费

lock-free 避免了互斥锁的系统调用——但代价是「缓存行在多核之间跳动」。8 核争抢同一个 atomic<int>——lock add 的延迟从 14ns 涨到 120ns。lock-free 的「free」指的是没有 OS 锁——不是没有代价。

哲学 3:CAS 是乐观锁——失败就要重试

CAS 的核心思想:先假设没有冲突、试试看、失败了再重试。 这和数据库的乐观锁、HTTP 的 conditional PUT——同一套哲学。乐观锁在高竞争下退化为「反复失败+重试」——这就是为什么高竞争场景下互斥锁反而更快。

# 10.4 速查表合集

atomic 关键 API:

API 调用方
is_lock_free() 运行期检测
is_always_lock_free 编译期常量 (C++17)
wait / notify_one / notify_all C++20 原子等待
fetch_add / fetch_sub 原子加减
compare_exchange_weak / strong CAS

下一篇:原子操作的指令说清了。下一篇进入 43.mutex 与条件变量——mutex 的底层 futex 实现原理、自旋锁 vs 阻塞锁的切换阈值、recursive_mutex 和 shared_mutex 的适用场景、condition_variable 的 spurious wakeup 根因与正确用法。

上次更新: 2026/06/10, 11:13:41
六大内存序详解
mutex与条件变量

← 六大内存序详解 mutex与条件变量→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式