Appearance
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(高频搞混题)
| volatile | atomic | |
|---|---|---|
| 作用 | 阻止编译器优化访问(不缓存到寄存器),每次真实读内存 | 提供原子性 + 可选内存序同步 |
| 线程安全 | ❌ 不能避免数据竞争,不保证可见性序与原子 | ✅ 专为多线程正确同步设计 |
| 跨线程 | 不能当同步机制 | 是的同步工具 |
| 用途 | 硬件寄存器、信号、嵌入式(与外部异步改动) | 共享计数、标志、发布订阅 |
结论: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 睡眠等待,适合持锁时间长;有上下文切换开销。