原子操作和内存序
本章建议阅读 多线程与任务编排 后进行阅读。
C++ 标准中的原子操作保证对某个原子对象的访问和操作不可分割,而内存序描述这些操作怎样和其他内存访问排序、同步。
C++ 提供的 API std::atomic<int> 能保护这个整数的并发读写,却不会自动保护旁边的普通变量。要让一个线程写入的数据安全地被另一个线程读取,通常还需要建立同步关系。C++ 标准把这些规则放在原子操作、内存序和数据竞争的章节中。
基本用途
#include <atomic>
std::atomic<int> visits{0};
void record_visit() {
visits.fetch_add(1);
}假设多个线程同时调用 record_visit,每次 fetch_add(1) 都会作为一个不可分割的操作更新 visits。如果用普通 int 做同样的递增,多个线程可能同时读到同一个旧值,再各自写回新值,造成更新丢失。
std::atomic 常见的操作有:
load():原子读取。store(value):原子写入。exchange(value):原子地写入新值,并返回旧值。fetch_add()、fetch_sub():原子地增减,并返回旧值。compare_exchange_weak()、compare_exchange_strong():比较并交换,简称 CAS。- C++20 起,部分原子类型还提供
wait()和notify_one()/notify_all()。
对于 std::atomic<int>,fetch_add 适合递增计数器。即使几个线程同时调用,它们的更新也不会互相覆盖。
std::atomic<int> visits{0};
void worker() {
for (int i = 0; i < 1000; ++i) {
visits.fetch_add(1, std::memory_order_relaxed);
}
}从数据竞争理解内存序
先看一种危险的场景:
int payload = 0;
std::atomic<bool> ready{false};
// 线程 A
payload = 42;
ready.store(true, std::memory_order_relaxed);
// 线程 B
if (ready.load(std::memory_order_relaxed)) {
use(payload);
}这段代码意图通过检测 ready,来确定 payload 是否已经写入完成。它们之间没有自动关系,关系是程序员用同步操作建立的。
因为 ready 通过 memory_order_relaxed 读写,因此对其的原子操作只能保证标志本身的读写安全,不能对 payload 做保证。因此,payload 依然可能发生数据竞争。
C++ 把线程内的顺序称为 sequenced-before,把某些跨线程操作之间的联系称为 synchronizes-with。如果一个操作通过这两类关系先于另一个操作,就形成 happens-before(先发生于)关系。要安全地传递普通数据,关键就是建立这样的关系。
下面用 release 和 acquire 建立这种关系:
int payload = 0;
std::atomic<bool> ready{false};
// 线程 A:生产者
void producer() {
payload = 42;
ready.store(true, std::memory_order_release);
}
// 线程 B:消费者
void consumer() {
while (!ready.load(std::memory_order_acquire)) {
// 等待 ready 变为 true
}
use(payload); // 这里可以安全读取
}在这个例子中,消费者的 acquire 读取到了生产者写入的 true。这个 acquire 与此前的 release 建立同步关系。由于 payload = 42 在 release 之前执行,消费者在 acquire 之后读取 payload,因而形成了正确的跨线程先后关系。
C++ 常见的内存序如下:
| 内存序 | 主要作用 |
|---|---|
memory_order_relaxed | 保证原子对象自身的操作不可分割,不建立额外同步 |
memory_order_consume | 依赖顺序;实际程序通常应避免使用 |
memory_order_acquire | 获取已发布的数据,常用于 load |
memory_order_release | 发布此前的写入,常用于 store |
memory_order_acq_rel | 同一个读改写操作同时具备 acquire 和 release 语义 |
memory_order_seq_cst | 顺序一致性;额外要求所有顺序一致操作处于同一个总序中 |
省略 memory_order 时,原子操作通常使用 memory_order_seq_cst。顺序一致性易于推理,适合作为起点,但它不会替程序设计出正确的同步协议。