原子操作、内存序与无锁基础

原子操作、内存序与无锁基础

时间:2026/04/09

关键词:std::atomic、CAS、memory order、acquire/release、seq_cst、false sharing、lock-free
核心目标:搞清楚“原子变量为什么不仅是线程安全的普通变量”,以及不同内存序到底在约束什么。


1. 为什么需要原子操作

并发程序里最核心的问题是:

  • 多个线程会同时访问共享数据
  • 如果至少一个线程写,且没有同步,就会产生数据竞争

例如:

1
2
int counter = 0;
// 多线程同时 ++counter;

这不是“结果偶尔不准”,而是未定义行为。

原子操作的价值就在于:

  • 某些共享读写可以不加互斥锁
  • 但仍然具备明确同步语义

2. std::atomic 是什么

最基本的用法:

1
2
3
#include <atomic>

std::atomic<int> counter{0};

它提供的不是“更快的 int”,而是:

  • 不会被撕裂的原子读写
  • 受内存模型约束的同步语义

常见操作:

  • load
  • store
  • fetch_add
  • fetch_sub
  • exchange
  • compare_exchange_weak
  • compare_exchange_strong

3. 原子不等于万能替代锁

原子适合的场景通常是:

  • 计数器
  • 标志位
  • 状态发布
  • 无锁数据结构中的基本原语

不适合的场景通常是:

  • 需要保护一整段复合逻辑
  • 需要同时维护多个共享变量的不变式
  • 业务逻辑复杂,容易写错同步关系

一句话:

  • 原子擅长保护“一个共享状态”
  • 锁擅长保护“一个临界区”

4. 最基本的原子操作

4.1 load / store

1
2
3
std::atomic<int> x{0};
x.store(10);
int v = x.load();

4.2 fetch_add

1
2
std::atomic<int> cnt{0};
cnt.fetch_add(1);

这比 cnt = cnt + 1 更重要,因为它是一个不可分割的原子读改写。

4.3 exchange

1
bool old = flag.exchange(true);

含义:

  • flag 设为 true
  • 返回旧值

5. CAS:无锁算法的核心原语

CAS 指 Compare-And-Swap(或 Compare-And-Exchange)。

5.1 直觉理解

1
2
3
如果当前值仍然等于 expected,
就把它改成 desired;
否则不改,并告诉我失败了。

5.2 C++ 里的写法

1
2
3
std::atomic<int> x{0};
int expected = 0;
bool ok = x.compare_exchange_strong(expected, 1);

如果成功:

  • x 变成 1
  • 返回 true

如果失败:

  • x 保持原值
  • expected 会被写成当前实际值

5.3 weakstrong

  • compare_exchange_weak:允许伪失败,适合循环重试
  • compare_exchange_strong:不允许伪失败,语义更直接

常见模式:

1
2
3
4
int expected = old;
while (!x.compare_exchange_weak(expected, new_value)) {
// expected 已被更新为当前值,继续重试
}

6. “原子”到底保证了什么

原子操作通常有两层含义:

6.1 操作本身不可分割

例如 fetch_add 不会被拆成“先读、再改、再写”被别的线程插进来。

6.2 它还可能携带同步顺序语义

这就进入内存序问题。

如果只知道“原子不会撕裂”,还远远不够。


7. 为什么会有内存序

现代 CPU 和编译器都会重排指令,只要不改变单线程可观察结果就行。
但并发下,如果没有同步约束,另一个线程看到的顺序可能和源码顺序不一样。

所以原子操作不只是“安全读写”,还承担:

  • 约束编译器重排
  • 约束 CPU 可见性顺序

这就是 memory_order 的意义。


8. 六种常见内存序

C++ 里最常见的有:

  • memory_order_relaxed
  • memory_order_consume
  • memory_order_acquire
  • memory_order_release
  • memory_order_acq_rel
  • memory_order_seq_cst

实际工程里最常用、最值得掌握的是:

  • relaxed
  • acquire
  • release
  • acq_rel
  • seq_cst

consume 在现代工程里很少主动使用。


9. relaxed:只保证原子性,不保证顺序

1
counter.fetch_add(1, std::memory_order_relaxed);

适合:

  • 纯计数
  • 统计量
  • 不需要借此发布其他数据

不适合:

  • “先写数据,再置位通知别人读取”

因为 relaxed 不保证其他普通内存写入的可见顺序。


10. release / acquire:发布与获取

这是最重要的一组。

10.1 发布方:release

1
2
data = 42;
ready.store(true, std::memory_order_release);

含义可以粗略理解为:

  • 在这之前的写入,不能被重排到这个 store 之后

10.2 获取方:acquire

1
2
3
if (ready.load(std::memory_order_acquire)) {
use(data);
}

如果这个 acquire load 读到了对应的 release store 写入的值,那么发布前的写入也对当前线程可见。

10.3 最经典的发布-订阅模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <atomic>

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

void producer() {
data = 42;
ready.store(true, std::memory_order_release);
}

void consumer() {
while (!ready.load(std::memory_order_acquire)) {}
// 这里可以安全看到 data = 42
}

这就是 acquire/release 最核心的使用场景。


11. acq_rel:读改写操作常用

对于像 fetch_addexchange、CAS 这样的读改写操作,经常用:

1
std::memory_order_acq_rel

它表示:

  • 对之前的写有 release 效果
  • 对之后的读有 acquire 效果

适合:

  • 需要同时承担“发布 + 获取”角色的 RMW 操作

12. seq_cst:最强也最直观

1
x.store(1, std::memory_order_seq_cst);

它提供最强、最容易理解的全局顺序语义。
可以先粗略理解为:

  • 所有 seq_cst 原子操作在所有线程看来像排成了一条总顺序

优点:

  • 最不容易想错

缺点:

  • 可能比更弱顺序更保守

经验上:

  • 不确定时可以先用 seq_cst
  • 真有性能证据,再考虑是否降到 acquire/release 或 relaxed

13. 一个常见误区:原子变量保护不了“旁边的普通变量”,除非顺序写对

例如:

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

如果你这样写:

1
2
data = 42;
ready.store(true, std::memory_order_relaxed);

另一个线程即便读到 ready == true,也不一定能可靠看见 data == 42
因为这里只保证 ready 本身原子,不保证普通变量 data 的可见顺序。

所以:

  • “用一个原子标志通知别人去读普通数据”
    这个模式必须认真选内存序

14. 自旋等待与忙等

原子标志很容易写出自旋:

1
2
while (!ready.load(std::memory_order_acquire)) {
}

这在短等待场景可能可接受,但有风险:

  • 浪费 CPU
  • 争抢资源
  • 等待时间一长非常低效

所以如果等待时间不可控,通常更适合:

  • 条件变量
  • futex / event
  • 更高层并发原语

15. atomic_flag 与自旋锁

最简单的原子标志是:

1
std::atomic_flag lock = ATOMIC_FLAG_INIT;

可实现一个最简单自旋锁:

1
2
3
4
5
6
while (lock.test_and_set(std::memory_order_acquire)) {
}

// critical section

lock.clear(std::memory_order_release);

但要明确:

  • 这只是帮助理解 acquire/release 的经典例子
  • 真实工程里自旋锁并不一定是好选择

原因包括:

  • 高竞争下性能糟糕
  • 容易烧 CPU
  • 可能不公平

16. lock-free 不等于更快

这是并发里最常见的误判之一。

“无锁”通常只表示:

  • 线程推进不依赖传统互斥锁

它不自动意味着:

  • 更低延迟
  • 更高吞吐
  • 更少 cache traffic

实际上,原子和 CAS 往往会带来:

  • cache line 抖动
  • 重试开销
  • 更难维护的代码

所以工程经验是:

先用正确、清晰的同步方案,再证明锁真的成了瓶颈,才考虑无锁化。


17. false sharing 在原子场景里更常见

多个线程频繁更新不同的原子变量,如果这些变量落在同一个 cache line,也会很慢。

例如:

1
2
3
4
struct Counters {
std::atomic<long long> a;
std::atomic<long long> b;
};

如果两个线程分别只改 ab,仍可能严重互相干扰。

常见缓解方式:

1
2
3
struct alignas(64) Counter {
std::atomic<long long> value{0};
};

或者:

  • 每线程本地累加
  • 最后统一合并

18. 原子引用计数与 shared_ptr

shared_ptr 之所以比 unique_ptr 重,很大一部分原因就在于:

  • 它内部常涉及线程安全的引用计数更新

也就是说,很多“我只是图省事用了 shared_ptr”的代码,实际上已经把原子开销带进来了。

所以在性能敏感路径里:

  • 不要把共享所有权当默认方案

19. 一个高频模式:单生产者发布结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Result {
int value;
};

Result result;
std::atomic<bool> done{false};

void producer() {
result.value = 123;
done.store(true, std::memory_order_release);
}

void consumer() {
while (!done.load(std::memory_order_acquire)) {}
// 这里读取 result.value
}

这类模式是 acquire/release 最值得优先熟练掌握的用法。


20. 一个高频模式:纯统计计数

1
2
3
4
5
6
7
std::atomic<long long> counter{0};

void worker() {
for (int i = 0; i < 100000; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
}
}

这里若只是最后读总数,relaxed 往往就足够,因为你并不依赖这个计数去同步其他数据。


21. 常见误区

21.1 “原子 = 不需要思考同步”

错。
原子只解决了一部分问题,真正难的是:

  • 你想建立什么可见性关系
  • 你要不要顺序保证

21.2 “我用了原子,就能保护一大片普通变量”

只有当发布/获取关系写对时才成立。

21.3 “lock-free 一定更先进”

并发工程里先进不先进不重要,重要的是:

  • 正确
  • 可维护
  • 有实际性能收益

21.4 “volatile 可以替代 atomic

不能。
volatile 不是线程同步原语。


22. 一页总结

原子操作最重要的不是 API 数量,而是建立这条理解链:

  1. 原子保证操作本身不会被数据竞争破坏
  2. 内存序决定不同线程看到这些操作及相关普通内存的顺序关系
  3. relaxed 只保原子性,不保发布顺序
  4. release/acquire 是最常用的发布-获取模型
  5. seq_cst 最直观,适合先写对
  6. 无锁不是性能银弹,false sharing 和 CAS 重试都可能很贵

如果只记两个最高频结论:

  • 纯计数:优先考虑 relaxed
  • 发布数据给别的线程:优先考虑 release/acquire

23. 建议继续补充的相关主题

和本篇衔接最紧密的内容:

  1. C++ 内存模型正式定义
  2. ABA 问题
  3. 无锁队列 / Michael-Scott Queue
  4. hazard pointers / epoch reclamation
  5. atomic_refstd::barrier

24. 参考资料

  1. cppreference: std::atomic
    https://en.cppreference.com/w/cpp/atomic/atomic

  2. cppreference: memory order
    https://en.cppreference.com/w/cpp/atomic/memory_order

  3. C++ Core Guidelines, Concurrency
    https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines