Skip to content

原子操作和内存序 ​

本章建议阅读 多线程与任务编排 后进行阅读。

C++ 标准中的原子操作保证对某个原子对象的访问和操作不可分割,而内存序描述这些操作怎样和其他内存访问排序、同步。

C++ 提供的 API std::atomic<int> 能保护这个整数的并发读写,却不会自动保护旁边的普通变量。要让一个线程写入的数据安全地被另一个线程读取,通常还需要建立同步关系。C++ 标准把这些规则放在原子操作、内存序和数据竞争的章节中。

基本用途 ​

cpp
#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 适合递增计数器。即使几个线程同时调用,它们的更新也不会互相覆盖。

cpp
std::atomic<int> visits{0};

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

从数据竞争理解内存序 ​

先看一种危险的场景:

cpp
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 建立这种关系:

cpp
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。顺序一致性易于推理,适合作为起点,但它不会替程序设计出正确的同步协议。

Released under MIT License