内存顺序对比表

内存顺序 适用操作 同步保证 典型用途
seq_cst 任意 全局顺序一致性 默认选项,需要强保证时
acquire 加载(load) 确保在该加载操作之后才读取非原子数据,以保证看到生产者线程Release操作之前的所有写入 读取发布数据(配合release)
release 存储(store) 保之前的所有内存操作(如数据写入)在该原子操作前完成 发布数据(配合acquire)
acq_rel 读-修改-写 兼具acquire+release 原子RMW操作
consume 加载(load) 依赖顺序(理论) 不推荐使用
relaxed 任意 仅原子性 计数器等无同步需求

1. std::memory_order_relaxed

最弱的内存序,仅保证原子性和修改顺序一致性,不提供同步和顺序约束。

示例1: 计数器

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

3.1 std::memory_order_acquire 和 std::memory_order_release的例子中,假如线程2 ready.load(std::memory_order_acquire)改成ready.load(std::memory_order_relax)会有问题吗?

假设重排:

consumer 中:

  1. 读取 ready 的值为 true(但用的是 relaxed,所以没有任何屏障阻止之后的读操作重排到之前)。

  2. 读取 data 得到指针 p

  3. 读取 *p(即 p 指向的内存)。

然而,由于编译器和处理器的优化,步骤1(relaxed 加载)不能阻止下面的乱序:

  • 编译器和处理器可能把步骤3(*p)的重排到步骤1(ready.load(relaxed))之前执行(只要在单线程内语义不变)。这意味着可能在 ready 还没有为 true 的时候就去读 *p(但此时 p 还没有被设置? 注意我们还有 data 的读取,这里还需要进一步分析)。

更准确的乱序可能性:

  • producer 中的写操作可能被重排(但 release 会阻止重排到 store(true) 之后)。

  • consumer 中,由于 ready.load(relaxed) 没有获得语义,它之后的任何读操作(包括 data.load(relaxed)*p)都可能被重排到它之前。但是,即使重排到它之前,在循环等待时我们只关心 ready 的值,所以不影响等待逻辑。但是最关键的是:​即使读到了 readytrue,却不能保证看到 producer 线程在 release 之前写入的数据(因为缺少 acquire 操作带来的可见性保证)​。

另一种错误:延迟的写入可见性

  • consumer 线程执行 ready.load(relaxed) 并读到了 true,但由于没有 acquire 语义,其他核心可能还没有看到 producer 线程在 release 之前对 *p 的写入(42)。

  • 所以,consumer 读取 *p 时可能读到旧值(比如内存中该地址尚未更新为42)或者初始值(比如0或垃圾值),导致断言失败。

4. std::memory_order_acq_rel

用于读-修改-写操作,同时具有获取和释放语义。即,它既同步之前的释放操作,又同步之后的获取操作。

示例3: 自旋锁实现

class SpinLock {
}

在顺序一致性下,所有操作(#1到#6)都按一个单一的全局顺序执行,且每个线程的操作顺序在全局顺序中保持不变。因此,在read_x_then_y中,看到x为真后,y一定为真(因为#1在#2之前发生)?但实际并非如此,因为线程a和b是并行的,两个写操作顺序未定。

然而,由于是顺序一致性,全局顺序中要么#1在#2之前,要么#2在#1之前。如果是#1在#2之前,那么read_y_then_x在循环退出(即看到#2为真)后,检查#1(x)也必定为真,因为#1在#2之前发生,所以#1已经被执行。反之亦然。因此,两个线程read_x_then_yread_y_then_x中至少有一个会执行++z。所以assert(z.load() != 0)永远不会触发。

但是,如果使用更弱的内存序(例如release/acquire),则上述断言可能触发。因为弱内存序下,不同的线程可能看到不同的操作顺序,可能导致一个线程看到x为真而y仍为假,另一个线程看到y为真而x仍为假,那么两个if条件都不成立,z将保持0。

总结

  • relaxed:仅原子,无同步。

  • release/acquire:配对使用,在两个线程之间建立同步关系。

  • acq_rel:用于读-修改-写,同时具有获取和释放语义。

  • seq_cst:全局顺序一致,最强约束,默认选项。

注意:实际编程中,应尽量使用默认的seq_cst,除非性能测试表明需要更弱的内存序。更弱的内存序虽然能提升性能,但正确性难以保证。

void incorrect_producer_relaxed() {
    data = 42;
    // 危险:使用relaxed存储,没有同步语义
    ready.store(true, std::memory_order_relaxed);
}

void correct_consumer() {
    while (!ready.load(std::memory_order_acquire)) ; // 使用acquire
    std::cout << data << std::endl; // 可能输出0,而不是42!
}
#include <atomic>
#include <thread>
#include <iostream>

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

void write_x() {
    x.store(true, std::memory_order_seq_cst);
}

void write_y() {
    y.store(true, std::memory_order_seq_cst);
}

void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst)) 
        ;
    if (y.load(std::memory_order_seq_cst))
        ++z;
}

void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst)) 
        ;
    if (x.load(std::memory_order_seq_cst))
        ++z;
}

int main() {
    std::thread a(write_x);
    std::thread b(write_y);
    std::thread c(read_x_then_y);
    std::thread d(read_y_then_x);
    a.join(); b.join(); c.join(); d.join();
    std::cout << "z = " << z << std::endl;
}

请注意在上述例子中,还是有可能因为cache之间的同步延迟导致结果为0,即运行read_x_then_y线程cpu本地cache有y的旧值,运行read_y_then_x的线程cpu本地cache有x的旧值。seq_cst情况下,不会发生指令重排