浏览知识库目录

C++

内存序

内存序

C++ 的原子操作不仅保证“读写不可分割”,还通过 std::memory_order 规定:当前线程中的普通内存访问,何时、以什么方式对其他线程可见。

理解内存序的核心不是“CPU 有没有重新排序”,而是:一个线程中的写入,另一个线程在什么条件下被保证能观察到。

1. 为什么需要内存序

下面是一个经典的线程通信例子:

#include <atomic>
#include <cassert>

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)) {
    }
    assert(data == 42);
}

这里有两个问题:

  1. ready 是原子的,因此对它的读写不会产生数据竞争。
  2. data 不是原子的,但 release/acquire 建立了跨线程同步,因此读取 data 是安全的。

同步链条为:

data = 42
    ↓ sequenced-before
ready.store(true, release)
    ↓ synchronizes-with
ready.load(acquire)
    ↓ sequenced-before
读取 data

当 acquire load 读到了 release store 写入的值时,release 之前的操作 happens-before acquire 之后的操作。

2. 六种内存序总览

内存序 原子性 排序与同步能力 常见用途
relaxed 不提供跨线程同步 计数器、统计信息
consume 数据依赖排序 实践中通常不建议使用
acquire 后续操作不能越过它 接收其他线程发布的数据
release 此前操作不能越过它 发布数据
acq_rel 同时 acquire 与 release 原子读改写操作
seq_cst 额外提供全局一致顺序 默认、安全优先

3. memory_order_relaxed

relaxed 只保证:

  • 当前原子对象的操作是原子的;
  • 当前原子对象存在一致的修改顺序;
  • 不保证其他普通变量的可见性;
  • 不建立线程间同步关系。
std::atomic<int> counter{0};

void work()
{
    counter.fetch_add(1, std::memory_order_relaxed);
}

这适合统计完成次数。每次递增不会丢失,但不能通过 counter 推断其他数据已经准备完成。

错误示例

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

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

void consumer()
{
    if (ready.load(std::memory_order_relaxed)) {
        std::cout << data; // 数据竞争,行为未定义
    }
}

即使消费者读到 ready == true,也没有同步关系保证它能安全读取 data

relaxed 并非“完全无序”

对同一个原子变量,所有修改仍存在统一的 modification order

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

x.store(1, std::memory_order_relaxed);
x.store(2, std::memory_order_relaxed);

其他线程不能把这两个修改解释成先 2 后 1。但不同原子变量之间没有统一顺序保证。

4. memory_order_release

release 通常用于原子写操作:

flag.store(true, std::memory_order_release);

它保证当前线程中位于 release 之前的读写,不能在 C++ 抽象机意义上移动到 release 之后。

release 主要用于“发布”数据:

payload = create_payload();
ready.store(true, std::memory_order_release);

release 不意味着“立即刷新所有缓存”。它规定的是:一旦匹配的 acquire 观察到了这个发布操作,之前的数据也必须可见。

5. memory_order_acquire

acquire 通常用于原子读操作:

if (flag.load(std::memory_order_acquire)) {
    use_shared_data();
}

它保证 acquire 之后的操作不能移动到 acquire 之前。

只有当 acquire load 确实读到对应 release store 或相应 release sequence 的结果时,二者才建立同步。如果 load 读到旧值,则没有同步关系。

6. memory_order_acq_rel

acq_rel 同时具有 acquire 和 release 语义,主要用于读—改—写操作:

counter.fetch_add(1, std::memory_order_acq_rel);
flag.exchange(true, std::memory_order_acq_rel);

state.compare_exchange_strong(
    expected,
    desired,
    std::memory_order_acq_rel
);
  • release 部分:发布当前操作之前的内容。
  • acquire 部分:接收当前操作所读取到的其他线程内容。

典型场景包括自旋锁、引用计数、无锁链表与队列、并发状态机。

普通 load 不能使用 release 或 acq_rel;普通 store 不能使用 acquire 或 acq_rel。

7. memory_order_seq_cst

seq_cst 是 sequentially consistent(顺序一致性),也是默认内存序:

x.store(1);       // 默认 seq_cst
int value = x.load();

它包含 acquire/release 能力,并为所有 seq_cst 操作建立一条所有线程都同意的全局总顺序。

std::atomic<bool> x{false};
std::atomic<bool> y{false};

int r1;
int r2;

void thread1()
{
    x.store(true, std::memory_order_seq_cst);
    r1 = y.load(std::memory_order_seq_cst);
}

void thread2()
{
    y.store(true, std::memory_order_seq_cst);
    r2 = x.load(std::memory_order_seq_cst);
}

这里不允许同时出现:

r1 == false && r2 == false

如果全部改成 relaxed,则两个线程都读到 false 是允许的。

seq_cst 并不是“禁止所有优化”,而是要求程序的可观察行为能解释为一个符合规则的全局顺序。

8. memory_order_consume

consume 原本用于提供比 acquire 更弱的数据依赖排序:

Node* p = head.load(std::memory_order_consume);
int value = p->value;

但数据依赖传播规则非常复杂,编译器长期难以正确、高效地实现。实践中编译器通常把 consume 当作 acquire。

工程中建议直接使用:

head.load(std::memory_order_acquire);

9. 三个重要关系

9.1 sequenced-before

描述同一线程内的顺序:

data = 42;
flag.store(true, std::memory_order_release);

data = 42 sequenced-before flag.store(...)

9.2 synchronizes-with

描述特定原子操作形成的跨线程同步:

release store synchronizes-with 读取该结果的 acquire load。

9.3 happens-before

这是判断普通内存访问是否安全的关键关系:

线程内 sequenced-before
        +
跨线程 synchronizes-with
        +
线程内 sequenced-before
        =
happens-before

两个线程对同一个普通变量进行冲突访问,其中至少一个是写,并且不存在 happens-before,就构成 data race。C++ 规定数据竞争是未定义行为。

10. compare_exchange 的成功与失败内存序

bool compare_exchange_weak(
    T& expected,
    T desired,
    std::memory_order success,
    std::memory_order failure
);

常见用法:

state.compare_exchange_weak(
    expected,
    desired,
    std::memory_order_acq_rel,
    std::memory_order_acquire
);
  • 成功时发生写入,使用成功内存序。
  • 失败时不发生写入,当前值被写回 expected,使用失败内存序。
  • 失败内存序不能是 releaseacq_rel,因为失败时没有内容可发布。

compare_exchange_weak 允许伪失败,因此通常放在循环中:

int expected = value.load(std::memory_order_relaxed);

while (!value.compare_exchange_weak(
    expected,
    expected + 1,
    std::memory_order_relaxed,
    std::memory_order_relaxed)) {
    // 失败后 expected 已更新为 value 的当前值
}

11. Release sequence

std::atomic<int> flag{0};
int data;

void thread1()
{
    data = 42;
    flag.store(1, std::memory_order_release);
}

void thread2()
{
    flag.fetch_add(1, std::memory_order_relaxed);
}

void thread3()
{
    if (flag.load(std::memory_order_acquire) == 2) {
        assert(data == 42);
    }
}

thread2fetch_add 是原子读改写操作。即使它使用 relaxed,也可以成为 release sequence 的一部分。

thread3 的 acquire load 读到这条 release sequence 中的值时,仍可与最初的 release store 建立同步,从而看到 data == 42

12. 原子变量不会自动保护普通变量

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

void writer()
{
    x = 1;
    counter.fetch_add(1, std::memory_order_relaxed);
}

void reader()
{
    if (counter.load(std::memory_order_relaxed) > 0) {
        std::cout << x; // 不安全
    }
}

counter 是原子的,只保证 counter 自身没有数据竞争,并不会自动保护 x

正确的发布与获取:

void writer()
{
    x = 1;
    counter.store(1, std::memory_order_release);
}

void reader()
{
    if (counter.load(std::memory_order_acquire) == 1) {
        std::cout << x; // 安全
    }
}

13. 自旋锁示例

class SpinLock {
public:
    void lock()
    {
        while (locked_.exchange(true, std::memory_order_acquire)) {
        }
    }

    void unlock()
    {
        locked_.store(false, std::memory_order_release);
    }

private:
    std::atomic<bool> locked_{false};
};

加锁成功时使用 acquire,用于获取上一个持锁线程发布的数据;解锁使用 release,保证临界区中的修改在解锁前完成。

14. atomic_thread_fence

std::atomic_thread_fence(std::memory_order_release);
std::atomic_thread_fence(std::memory_order_acquire);

栅栏可以对前后的多个操作施加排序,但正确建立跨线程同步仍需要原子操作参与。规则通常比直接使用 release store 和 acquire load 更难推理,因此除非实现底层并发组件,否则优先把内存序直接附着在原子操作上。

15. 编译器屏障与 CPU 屏障

内存序同时约束两个层面:

  • 编译器不能生成违反 C++ 内存模型的重排;
  • 机器指令必须在目标 CPU 上满足相应语义。

不同架构的成本不同:

  • x86/x64 内存模型较强,很多 acquire load、release store 不需要额外屏障指令;
  • ARM、POWER 等架构较弱,可能需要特殊加载、存储或屏障;
  • seq_cst 通常可能比 acquire/release 成本更高。

不要只根据某次 x86 汇编结果判断源代码是否正确。代码首先必须符合 C++ 内存模型。

16. 如何选择内存序

默认选择 seq_cst

如果性能没有被测量证明存在问题,默认的 seq_cst 最容易推理。

C++ 的 std::atomic 如果不指定内存序,默认使用:

std::memory_order_seq_cst

发布数据:release/acquire

data = value;
ready.store(true, std::memory_order_release);

// 另一线程
if (ready.load(std::memory_order_acquire)) {
    use(data);
}

仅关心原子值本身:relaxed

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

适用于统计计数、调试指标等不承担数据发布职责的场景。

读改写既接收又发布:acq_rel

state.fetch_or(mask, std::memory_order_acq_rel);

是否需要双向同步,必须根据算法证明,不能只凭“它看起来更强”。

17. 常见误区

volatile 可以实现线程同步

不可以。C++ 的 volatile 不提供原子性,也不建立 happens-before。

原子操作一定无锁

不一定:

std::atomic<T> value;
bool lock_free = value.is_lock_free();

原子性与无锁性是两个概念。

relaxed 就是不安全

不是。它对原子对象本身是安全的,只是不适合承担普通数据的发布和接收职责。

acquire 能看到所有线程的最新数据

不能这样理解。它只保证看到与匹配 release 之间由 happens-before 覆盖的效果。

更强的内存序一定能修复并发算法

不一定。内存序无法修复生命周期错误、ABA 问题、越界或错误的状态转换。

18. 记忆口诀

relaxed:只保证这个原子变量本身。
release:把此前的数据发布出去。
acquire:接收另一个线程此前发布的数据。
seq_cst:acquire/release 加上一条全局统一的原子操作顺序。

最后记住关键条件:

acquire 必须观察到相应的 release 写入或其 release sequence,二者才建立同步关系。

在工程中,除非正在实现高性能无锁结构,否则优先使用互斥锁或默认的 seq_cst。弱化内存序之前,应先写出明确的 happens-before 证明,再通过基准测试确认弱化确实带来了收益。