C++内存模型

1 C++ 内存模型

1.1 前言

1.1.1 为什么要学内存模型

多线程程序中,你可能遇到过这样的问题:

cpp 复制代码
// 线程1
flag = true;
data = 42;

// 线程2
if (flag) {
    use(data);  // data 一定是 42 吗?
}

答案是不一定。即使 flag 已经是 true,data 也可能还没被线程2看到。这不是 bug,而是现代 CPU 和编译器正常工作的结果。

C++ 内存模型就是用来回答这类问题的:在多线程环境下,一个线程对内存的写入,另一个线程什么时候、以什么顺序能看到?

不理解内存模型,写出的无锁代码要么有隐藏的数据竞争(在某些 CPU 上偶发崩溃),要么加了不必要的屏障(浪费性能)。

1.1.2 问题的根源:三层乱序

程序的执行顺序在三个层面都可能被打乱:

复制代码
你写的代码顺序
      ↓  [编译器重排:为优化调整指令顺序]
汇编指令顺序
      ↓  [CPU 乱序执行:超标量、乱序发射]
实际执行顺序
      ↓  [存储缓冲/Cache 不一致:写入对其他核不立即可见]
其他线程看到的顺序

C++ 内存模型提供了一套语言层面的工具,让程序员能精确控制这三层的可见性和顺序。


1.2 基础概念

1.2.1 内存位置(Memory Location)

C++ 内存模型首先定义了"内存位置":

  • 一个标量类型(int、指针、float 等)对象是一个内存位置
  • 相邻的非零位域(bitfield)构成一个内存位置
  • 零宽位域把位域分隔成不同的内存位置
cpp 复制代码
struct S {
    int a;          // 内存位置1
    int b;          // 内存位置2
    char c;         // 内存位置3
    unsigned x : 4; // 内存位置4(x 和 y 在同一位域)
    unsigned y : 4; //
    unsigned   : 0; // 零宽,强制分隔
    unsigned z : 8; // 内存位置5
};

关键规则 :两个线程访问同一个内存位置 ,且至少一个是写操作,且没有同步措施------这就是数据竞争(Data Race),属于未定义行为。

1.2.2 数据竞争(Data Race)

cpp 复制代码
int x = 0;

// 线程1            // 线程2
x = 1;             int y = x;   // ← 数据竞争!UB!

注意:未定义行为不是说结果不对,而是说任何事情都可能发生,包括:

  • 读到旧值
  • 读到新值
  • 程序崩溃
  • 编译器优化掉整段代码

1.2.3 同步操作(Synchronization Operations)

C++ 内存模型定义了哪些操作能建立线程间的同步关系:

  • std::mutexlock()/unlock()
  • std::atomic 的各种操作
  • std::thread::join()
  • std::condition_variablewait()/notify_all()
  • std::promise/std::future

通过这些操作,可以建立 happens-before 关系。


1.3 happens-before 关系

1.3.1 什么是 happens-before

happens-before 是内存模型的核心概念,表示"A 的结果对 B 一定可见"。

复制代码
A happens-before B
    ↓
A 的所有内存写入,在 B 执行时一定已经完成且可见

它由两部分组成:

sequenced-before(同一线程内):同一线程中,代码的顺序决定了 sequenced-before 关系。

cpp 复制代码
// 同一线程内:
int x = 1;        // A
int y = x + 1;    // B
// A sequenced-before B,因此 A happens-before B
// y 一定是 2

synchronizes-with(跨线程):通过同步操作建立跨线程的 happens-before。

cpp 复制代码
std::atomic<bool> flag{false};
int data = 0;

// 线程1
data = 42;              // A
flag.store(true);       // B(release)

// 线程2
while (!flag.load());   // C(acquire)
use(data);              // D

// B synchronizes-with C(release-acquire 建立同步)
// 因此:A happens-before B happens-before C happens-before D
// D 一定能看到 data == 42

1.3.2 传递性

happens-before 具有传递性:

复制代码
若 A happens-before B,B happens-before C
则 A happens-before C

这是构建复杂同步逻辑的基础。


1.4 内存序(Memory Order)

C++ 11 定义了 6 种内存序,用于 std::atomic 操作,控制该操作周围的重排范围。

cpp 复制代码
namespace std {
    enum class memory_order {
        relaxed,    // 最宽松
        consume,    // 数据依赖(不推荐使用,几乎等同 acquire)
        acquire,    // 获取语义
        release,    // 释放语义
        acq_rel,    // 获取+释放
        seq_cst     // 顺序一致(最严格,默认值)
    };
}

从宽松到严格:relaxed < consume < acquire/release < acq_rel < seq_cst

越严格,对重排的限制越多,性能开销越大。


1.5 Relaxed 序------只保证原子性

1.5.1 含义

memory_order_relaxed 只保证操作本身是原子的,不建立任何 happens-before 关系,允许任意重排。

cpp 复制代码
std::atomic<int> x{0}, y{0};

// 线程1
x.store(1, std::memory_order_relaxed);
y.store(1, std::memory_order_relaxed);

// 线程2
int a = y.load(std::memory_order_relaxed);
int b = x.load(std::memory_order_relaxed);

// 可能的结果:a=1, b=0
// 即:线程2看到了 y 的新值,但没看到 x 的新值
// 从线程2的视角,两个写入顺序可以颠倒

1.5.2 适用场景

计数器:只关心最终结果是原子累加,不关心顺序。

cpp 复制代码
std::atomic<int> counter{0};

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

多个线程同时 fetch_add,最终结果是正确的,因为 fetch_add 本身是原子的。不需要顺序保证,用 relaxed 即可。

标志位的最终可见性

cpp 复制代码
std::atomic<bool> keep_running{true};

void worker() {
    while (keep_running.load(std::memory_order_relaxed)) {
        // 做任务
    }
}

void stopper() {
    keep_running.store(false, std::memory_order_relaxed);
}

只需要 keep_running 最终变为 false 让 worker 停下,不需要精确的顺序保证,relaxed 足够。

1.5.3 不能用 relaxed 的场景

cpp 复制代码
// ❌ 错误:用 relaxed 实现发布数据
std::atomic<bool> ready{false};
int data = 0;

// 线程1
data = 42;
ready.store(true, std::memory_order_relaxed);  // ← 错!

// 线程2
while (!ready.load(std::memory_order_relaxed));
use(data);  // data 可能不是 42!

发布数据需要保证 data = 42 的写入对线程2可见,这需要 release/acquire。


1.6 Release-Acquire 序------最常用的同步模式

1.6.1 语义

  • Release(释放) :store 操作加 release 语义,保证该 store 之前 的所有内存操作不会被重排到该 store 之后。相当于在 store 前插入一道屏障。

  • Acquire(获取) :load 操作加 acquire 语义,保证该 load 之后 的所有内存操作不会被重排到该 load 之前。相当于在 load 后插入一道屏障。

    线程1:
    [普通写] [普通写] [普通写]
    ↑ ↑ ↑ 这些写入不会穿越 release 屏障
    ━━━━━━━━━━━━━━━━━━━ release store ━━━

    线程2:
    ━━━━━━━━━━━━━━━━━━━ acquire load ━━━
    ↓ ↓ ↓ 这些读取不会穿越 acquire 屏障
    [普通读] [普通读] [普通读]

1.6.2 Release-Acquire 建立 synchronizes-with

当线程2的 acquire load 读到了线程1的 release store 写入的值,就建立了 synchronizes-with 关系:

cpp 复制代码
std::atomic<bool> flag{false};
int data = 0;

// 线程1
data = 42;                                    // (1)
flag.store(true, std::memory_order_release);  // (2) release

// 线程2
while (!flag.load(std::memory_order_acquire)); // (3) acquire
int x = data;                                  // (4)

// (2) synchronizes-with (3)
// 因此 (1) happens-before (4)
// x 一定是 42

1.6.3 经典模式:发布-订阅

cpp 复制代码
struct Payload {
    int x, y, z;
};

std::atomic<Payload*> ptr{nullptr};

// 生产者线程
Payload* p = new Payload{1, 2, 3};
// 先完成数据的构造,再发布指针
ptr.store(p, std::memory_order_release);  // release

// 消费者线程
Payload* p;
while (!(p = ptr.load(std::memory_order_acquire)));  // acquire
// 这里能安全访问 p->x, p->y, p->z
// 因为 release-acquire 保证了构造的可见性
use(p->x);  // 一定是 1
use(p->y);  // 一定是 2
use(p->z);  // 一定是 3

1.6.4 RMW 操作的 acq_rel

读-改-写(RMW)操作(fetch_addcompare_exchange 等)同时读和写,需要 acq_rel

cpp 复制代码
std::atomic<int> lock{0};

void acquire_lock() {
    int expected = 0;
    // compare_exchange 是 RMW:读 lock,若等于 expected 则写 1
    while (!lock.compare_exchange_weak(
        expected, 1,
        std::memory_order_acq_rel,   // 成功时的内存序
        std::memory_order_relaxed))  // 失败时的内存序
    {
        expected = 0;
    }
}

void release_lock() {
    lock.store(0, std::memory_order_release);
}

1.7 Sequential Consistency------最强保证

1.7.1 含义

memory_order_seq_cst 是默认内存序,提供最强的保证:

  1. 包含 release 和 acquire 的所有保证
  2. 额外保证:所有 seq_cst 操作存在一个全局的单一顺序(total order),所有线程观察到的顺序是一致的
cpp 复制代码
std::atomic<bool> x{false}, y{false};
std::atomic<int> z{0};

// 线程1        // 线程2         // 线程3         // 线程4
x.store(true);  y.store(true);  if(x.load()) {   if(y.load()) {
                                  if(!y.load())    if(!x.load())
                                    z++;             z++;
                                }                }

// 用 seq_cst(默认)时:z 最终不可能是 2
// 因为所有 seq_cst 操作有全局一致的顺序
// 如果线程3看到 x=true 且 y=false,
// 线程4就不可能看到 y=true 且 x=false(否则两者看到的顺序矛盾)

// 用 release/acquire 时:z 可能是 2!
// 因为 release/acquire 不保证全局一致顺序

1.7.2 什么时候需要 seq_cst

大多数情况下 release/acquire 就够用,以下场景需要 seq_cst:

  • 需要所有线程观察到相同的操作顺序时
  • 不确定用哪种内存序时(安全的默认选择)
  • 正确性比性能更重要时

1.7.3 性能代价

在 x86 上,seq_cst store 会生成 MFENCELOCK XCHG 指令,比 release store(只需要 MOV)慢 5-10 倍。在 ARM 上差距更大。

cpp 复制代码
// x86 汇编对比

// seq_cst store(慢)
x.store(1);  →  LOCK XCHG [x], 1   或   MOV [x], 1; MFENCE

// release store(快)
x.store(1, memory_order_release);  →  MOV [x], 1

// relaxed store(最快)
x.store(1, memory_order_relaxed);  →  MOV [x], 1

注意:x86 的内存模型本身已经比较强(TSO),所以 release store 和 relaxed store 在 x86 上生成相同指令。在 ARM/PowerPC 上差别更显著。


1.8 Fence(内存屏障)

除了在原子操作上指定内存序,还可以单独插入 fence(屏障)。

1.8.1 std::atomic_thread_fence

cpp 复制代码
std::atomic_thread_fence(std::memory_order_release); // release fence
std::atomic_thread_fence(std::memory_order_acquire); // acquire fence
std::atomic_thread_fence(std::memory_order_seq_cst); // seq_cst fence

1.8.2 Fence 和原子操作内存序的关系

Fence 比在原子操作上指定内存序更强

cpp 复制代码
// 方式1:在 store 上指定 release(常见)
data = 42;
flag.store(true, std::memory_order_release);

// 方式2:用 release fence + relaxed store(等价但稍强)
data = 42;
std::atomic_thread_fence(std::memory_order_release);
flag.store(true, std::memory_order_relaxed);

// 方式2 中,fence 保护的范围比方式1更广
// 方式1只保证 flag store 之前的操作不重排到 flag store 之后
// 方式2保证 fence 之前的操作不重排到 fence 之后的任何 relaxed store

1.8.3 实用场景:批量发布

cpp 复制代码
// 一次性发布多个数据,用一个 fence 代替多个 release store
int a = 1, b = 2, c = 3;  // 普通变量

std::atomic<bool> ready{false};

// 生产者
a = 10; b = 20; c = 30;
std::atomic_thread_fence(std::memory_order_release);  // 一个 fence 保护所有写入
ready.store(true, std::memory_order_relaxed);

// 消费者
while (!ready.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);  // 一个 fence 保护所有读取
use(a); use(b); use(c);  // 都是安全的

1.9 std::atomic 详解

1.9.1 基本类型

cpp 复制代码
#include <atomic>

std::atomic<bool>     ab;
std::atomic<int>      ai;
std::atomic<long>     al;
std::atomic<float>    af;   // C++20 支持浮点原子操作
std::atomic<T*>       ap;   // 指针

// 便利别名
std::atomic_bool    // = std::atomic<bool>
std::atomic_int     // = std::atomic<int>
std::atomic_long    // = std::atomic<long>
std::atomic_size_t  // = std::atomic<size_t>

1.9.2 基本操作

cpp 复制代码
std::atomic<int> x{0};

// 存储
x.store(42);                              // seq_cst(默认)
x.store(42, std::memory_order_release);   // release
x = 42;                                   // 等价于 seq_cst store

// 加载
int v = x.load();                         // seq_cst(默认)
int v = x.load(std::memory_order_acquire); // acquire
int v = x;                                // 等价于 seq_cst load

// 交换(RMW)
int old = x.exchange(99);                 // 原子地设为99,返回旧值

// 读-改-写:加法
int old = x.fetch_add(1);                // 原子地+1,返回旧值
int old = x.fetch_sub(1);                // 原子地-1
int old = x.fetch_and(0xFF);             // 原子地按位与
int old = x.fetch_or(0x01);              // 原子地按位或
int old = x.fetch_xor(0x01);             // 原子地按位异或

// 便捷运算符(返回新值,seq_cst)
++x; x++; --x; x--;
x += 5; x -= 5; x &= 0xFF; x |= 0x01;

1.9.3 CAS(Compare-And-Swap)------无锁编程的基石

CAS 是最强大的原子操作:

cpp 复制代码
bool compare_exchange_strong(T& expected, T desired,
                              memory_order success,
                              memory_order failure);
bool compare_exchange_weak(T& expected, T desired,
                           memory_order success,
                           memory_order failure);

语义:

  • 如果当前值 == expected,则将其改为 desired,返回 true
  • 如果当前值 != expected,则将当前值写入 expected,返回 false
cpp 复制代码
std::atomic<int> x{10};

int expected = 10;
bool ok = x.compare_exchange_strong(expected, 20);
// ok == true,x 现在是 20

expected = 10;  // 重置
ok = x.compare_exchange_strong(expected, 30);
// ok == false,expected 现在是 20(被更新为当前值),x 不变

weak vs strong

  • strong:保证只有在值真的不等时才失败(spurious failure 不发生)
  • weak:允许偶尔在值相等时也失败(spurious failure),但在循环中性能更好
cpp 复制代码
// 正确用法:weak 在循环中使用
int expected = x.load(memory_order_relaxed);
while (!x.compare_exchange_weak(expected, expected * 2,
                                memory_order_acq_rel,
                                memory_order_relaxed)) {
    // expected 已被自动更新为最新值,继续重试
}

1.9.4 用 CAS 实现无锁栈

cpp 复制代码
template<typename T>
class LockFreeStack {
    struct Node {
        T data;
        Node* next;
        Node(T d) : data(std::move(d)), next(nullptr) {}
    };

    std::atomic<Node*> head{nullptr};

public:
    void push(T data) {
        Node* new_node = new Node(std::move(data));
        // CAS 循环:尝试把 head 换成 new_node
        new_node->next = head.load(std::memory_order_relaxed);
        while (!head.compare_exchange_weak(
            new_node->next, new_node,        // 若 head == new_node->next,则改为 new_node
            std::memory_order_release,       // 成功时 release(发布 new_node 的数据)
            std::memory_order_relaxed)) {    // 失败时 relaxed(next 已被更新)
            // 失败了,new_node->next 已被自动更新为最新的 head
            // 继续重试
        }
    }

    std::optional<T> pop() {
        Node* old_head = head.load(std::memory_order_relaxed);
        while (old_head &&
               !head.compare_exchange_weak(
                   old_head, old_head->next,
                   std::memory_order_acquire,  // 成功时 acquire(获取 old_head 的数据)
                   std::memory_order_relaxed)) {
        }
        if (!old_head) return std::nullopt;
        T result = std::move(old_head->data);
        delete old_head;  // 注意:这里有 ABA 问题,实际需要更复杂的内存回收
        return result;
    }
};

1.9.5 is_lock_free 与 is_always_lock_free

cpp 复制代码
std::atomic<int> x;
x.is_lock_free();  // 运行时查询

// C++17:编译期查询
static_assert(std::atomic<int>::is_always_lock_free);
static_assert(std::atomic<long long>::is_always_lock_free);

// 大类型可能不是 lock-free
struct Big { int arr[10]; };
std::atomic<Big> b;
b.is_lock_free();  // 可能是 false,内部用了锁

如果不是 lock-free,std::atomic 内部会用互斥锁实现,性能等同于加锁。


1.10 常见错误模式

1.10.1 Double-Checked Locking(双重检查锁)的正确写法

cpp 复制代码
// ❌ 经典错误:C++11 之前的写法,有数据竞争
Singleton* instance = nullptr;
std::mutex mtx;

Singleton* getInstance() {
    if (instance == nullptr) {        // 第一次检查,无锁,数据竞争!
        std::lock_guard<std::mutex> lock(mtx);
        if (instance == nullptr) {    // 第二次检查,有锁
            instance = new Singleton();  // 但这行可能被重排:先发布指针,再构造对象
        }
    }
    return instance;
}

// ✅ 正确写法1:用 atomic
std::atomic<Singleton*> instance{nullptr};
std::mutex mtx;

Singleton* getInstance() {
    Singleton* p = instance.load(std::memory_order_acquire);
    if (p == nullptr) {
        std::lock_guard<std::mutex> lock(mtx);
        p = instance.load(std::memory_order_relaxed);  // 锁内不需要 acquire
        if (p == nullptr) {
            p = new Singleton();
            instance.store(p, std::memory_order_release);  // 发布时 release
        }
    }
    return p;
}

// ✅ 正确写法2:最简单,利用 C++11 的静态局部变量保证
Singleton* getInstance() {
    static Singleton instance;  // C++11 保证线程安全的初始化
    return &instance;
}

1.10.2 错误的发布模式

cpp 复制代码
// ❌ 错误:store 和 load 内存序不匹配
std::atomic<int*> ptr{nullptr};
int data = 0;

// 线程1
data = 42;
ptr.store(new int(data), std::memory_order_relaxed);  // ← 应该是 release

// 线程2
int* p;
while (!(p = ptr.load(std::memory_order_acquire)));    // acquire 这里没用
use(*p);  // 可能看到未初始化的数据

// ✅ 正确:release + acquire 配对
// 线程1
data = 42;
ptr.store(new int(data), std::memory_order_release);  // release

// 线程2
int* p;
while (!(p = ptr.load(std::memory_order_acquire)));   // acquire
use(*p);  // 安全

1.10.3 忘记 acquire 导致读到旧值

cpp 复制代码
// ❌ 错误:load 用了 relaxed,没有获取语义
std::atomic<bool> flag{false};
int data = 0;

// 线程1
data = 42;
flag.store(true, std::memory_order_release);  // release(正确)

// 线程2
while (!flag.load(std::memory_order_relaxed));  // ← 应该是 acquire!
use(data);  // 不安全,data 的写入可能对线程2不可见

1.10.4 ABA 问题

无锁数据结构中的经典陷阱:

cpp 复制代码
// 场景:无锁栈
// 初始状态:head → A → B → C

// 线程1:准备 pop,读到 head = A,expected = A,desired = A->next = B
//        在执行 CAS 之前被挂起

// 线程2:执行了 pop(A), pop(B), push(A)
//        状态变成:head → A → C(A 被重新入栈,但 A->next 现在是 C)

// 线程1 恢复:执行 CAS(head, A, B)
//        head 仍然是 A,CAS 成功!
//        但 head 被设为了 B,而 B 已经被 pop 并可能被 delete 了!
//        head → B(悬空指针!)

解决方案:带版本号的指针(tagged pointer):

cpp 复制代码
// 把指针的低位(由于对齐,低几位是0)用来存版本号
// 或者使用 128bit CAS(DWCAS)同时更新指针和版本号
struct TaggedPointer {
    Node* ptr;
    uintptr_t tag;
};
std::atomic<TaggedPointer> head;

// pop 时同时更新 tag,防止 ABA
TaggedPointer old_head = head.load();
TaggedPointer new_head = {old_head.ptr->next, old_head.tag + 1};
head.compare_exchange_weak(old_head, new_head, ...);

1.11 实用同步模式

1.11.1 一次性初始化(std::call_once)

cpp 复制代码
#include <mutex>

std::once_flag init_flag;
std::shared_ptr<Resource> resource;

void initialize() {
    std::call_once(init_flag, [] {
        resource = std::make_shared<Resource>();
    });
}

// 多线程同时调用 initialize():
// 只有一个线程执行 lambda,其他线程等待它完成
// 完成后所有线程都能安全访问 resource

1.11.2 生产者-消费者(带队列)

cpp 复制代码
template<typename T>
class BoundedQueue {
    std::vector<T> buf;
    std::atomic<size_t> head{0}, tail{0};
    size_t cap;

public:
    BoundedQueue(size_t cap) : buf(cap), cap(cap) {}

    // 单生产者单消费者(SPSC)无锁版本
    bool push(T val) {
        size_t t = tail.load(std::memory_order_relaxed);
        size_t next = (t + 1) % cap;
        if (next == head.load(std::memory_order_acquire))  // 队满
            return false;
        buf[t] = std::move(val);
        tail.store(next, std::memory_order_release);  // 发布新 tail
        return true;
    }

    bool pop(T& val) {
        size_t h = head.load(std::memory_order_relaxed);
        if (h == tail.load(std::memory_order_acquire))  // 队空
            return false;
        val = std::move(buf[h]);
        head.store((h + 1) % cap, std::memory_order_release);
        return true;
    }
};

1.11.3 读写计数器(无锁读取,有锁写入)

cpp 复制代码
class Statistics {
    std::atomic<uint64_t> requests{0};
    std::atomic<uint64_t> errors{0};
    std::atomic<uint64_t> bytes{0};

public:
    void record_request(uint64_t size, bool success) {
        requests.fetch_add(1, std::memory_order_relaxed);
        bytes.fetch_add(size, std::memory_order_relaxed);
        if (!success)
            errors.fetch_add(1, std::memory_order_relaxed);
    }

    struct Snapshot {
        uint64_t requests, errors, bytes;
    };

    Snapshot snapshot() const {
        // seq_cst load 保证读到最新值且三者顺序一致
        return {
            requests.load(std::memory_order_seq_cst),
            errors.load(std::memory_order_seq_cst),
            bytes.load(std::memory_order_seq_cst)
        };
    }
};

1.11.4 Seqlock(读优化的读写锁)

适合读多写少、写操作短暂的场景:

cpp 复制代码
class SeqLock {
    std::atomic<unsigned> seq{0};  // 奇数表示写进行中,偶数表示稳定
    int x, y;                      // 被保护的数据

public:
    // 写操作
    void write(int new_x, int new_y) {
        unsigned s = seq.load(std::memory_order_relaxed);
        seq.store(s + 1, std::memory_order_release);  // 变为奇数,通知读者
        x = new_x;
        y = new_y;
        seq.store(s + 2, std::memory_order_release);  // 变为偶数,写完成
    }

    // 读操作:乐观读,读完验证序列号是否变化
    std::pair<int,int> read() const {
        while (true) {
            unsigned s1 = seq.load(std::memory_order_acquire);
            if (s1 & 1) { continue; }  // 写进行中,等待

            int rx = x, ry = y;        // 读数据

            unsigned s2 = seq.load(std::memory_order_acquire);
            if (s1 == s2) return {rx, ry};  // 读期间没有写,数据有效
            // 否则重试
        }
    }
};

读操作完全无锁,多个读者不互斥。写操作只需改两次序列号。


1.12 硬件视角:为什么需要内存模型

1.12.1 Store Buffer(存储缓冲)

现代 CPU 有 Store Buffer:写操作先写入 Store Buffer,稍后才刷入 Cache。

复制代码
CPU Core 0               CPU Core 1
┌──────────────┐         ┌──────────────┐
│  Store Buffer│         │  Store Buffer│
│  x=1 (待刷) │         │  y=1 (待刷) │
└──────┬───────┘         └──────┬───────┘
       ↓                        ↓
       └────────────────────────┘
                   ↓
            共享 Cache / 内存
               x=0, y=0    ← Store Buffer 还没刷入

Core 0 写了 x=1,但 Core 1 的 load x 可能还是从 Cache 读到 0,因为 Store Buffer 还没刷入。

Memory Barrier(内存屏障) 强制刷新 Store Buffer,确保写入对其他核可见。

1.12.2 x86 vs ARM 的内存模型差异

复制代码
x86(TSO - Total Store Order)
- store-load 可以重排(写后读可乱序)
- load-load、store-store、load-store 不重排
- release store 几乎免费,只是普通 MOV
- seq_cst store 需要 MFENCE,昂贵

ARM(弱内存模型)
- 几乎所有重排都允许
- 需要大量 DMB(Data Memory Barrier)指令
- release = stlr 指令,acquire = ldar 指令
- 原子操作开销比 x86 更高

这解释了为什么同样的无锁代码在 x86 上测试没问题,在 ARM 上出现数据竞争:x86 的硬件内存模型比 C++ 内存模型要求的更强,自动帮你做了部分同步。

1.12.3 编译器重排

除了硬件,编译器也会重排:

cpp 复制代码
int x = 0, y = 0;

// 原始代码
x = 1;
y = 1;

// 编译器可能重排为(因为两者无依赖关系)
y = 1;
x = 1;

std::atomic 的内存序同时约束了编译器和 CPU 的重排行为。


1.13 调试内存模型问题

1.13.1 AddressSanitizer(ASan)检测数据竞争

bash 复制代码
# ThreadSanitizer(TSan)专门检测数据竞争
clang++ -fsanitize=thread -g your_program.cpp -o your_program
./your_program

# 输出示例(检测到竞争):
# WARNING: ThreadSanitizer: data race (pid=1234)
#   Write of size 4 at 0x... by thread T2:
#     #0 thread_func1 main.cpp:10
#   Previous read of size 4 at 0x... by thread T1:
#     #0 thread_func2 main.cpp:20

TSan 是调试数据竞争最有效的工具,强烈推荐在测试阶段使用。

1.13.2 常见的竞争模式识别

cpp 复制代码
// 竞争模式1:无保护的全局变量
int counter = 0;               // ❌
void inc() { counter++; }      // ❌ 两个线程同时调用

// 竞争模式2:检查后操作
if (ptr != nullptr) {          // 线程1检查
    // 线程2在这里 delete ptr
    use(*ptr);                 // ❌ use-after-free
}

// 竞争模式3:非原子的复合操作
if (x > 0) x--;               // ❌ 读-改-写不是原子的

// 竞争模式4:错误的内存序导致逻辑竞争
flag.store(true, relaxed);    // ❌ 需要 release
data.load(relaxed);           // ❌ 需要 acquire

1.14 总结与选型指南

1.14.1 内存序选择决策树

复制代码
需要原子操作
│
├── 只需要操作本身是原子的,不需要同步其他内存?
│   → memory_order_relaxed(计数器、标志位)
│
├── 发布数据给其他线程(写方)?
│   → memory_order_release(store 操作)
│
├── 获取其他线程发布的数据(读方)?
│   → memory_order_acquire(load 操作)
│
├── 同时读和写(RMW 操作),且需要同步?
│   → memory_order_acq_rel(fetch_add/CAS 等)
│
├── 需要所有线程看到一致的全局顺序?
│   → memory_order_seq_cst(默认值)
│
└── 不确定用哪个?
    → memory_order_seq_cst(最安全的选择)

1.14.2 各内存序对比

内存序 建立 happens-before 防止重排 性能(x86) 适用操作
relaxed 最快 计数器、独立标志
acquire 单向(后) 后面不往前移 load(读取发布的数据)
release 单向(前) 前面不往后移 store(发布数据)
acq_rel 双向 前后都不移 RMW 操作
seq_cst 全局一致 全局一致顺序 需要全局顺序时

1.14.3 核心原则

原则1:有数据共享就有潜在竞争。 任何两个线程访问同一个非 atomic 变量且至少一个是写,就是 UB。

原则2:release 和 acquire 必须配对。 单独用 release store 而对端用 relaxed load,同步无效。

原则3:优先用高层同步原语。 std::mutexstd::condition_variablestd::shared_ptr 已经处理好了内存序,不需要手动考虑。只有在对性能要求极高的热路径才考虑手写 atomic。

原则4:先写正确,再优化内存序。 默认用 seq_cst(atomic 的默认值),验证正确后再分析是否可以放宽到 acquire/release 或 relaxed。

原则5:用 TSan 验证。 人工检查内存模型正确性极易出错,ThreadSanitizer 是必备工具。

相关推荐
Hello!!!!!!2 小时前
C++基础(十二)——标准库算法
c++·算法
Tairitsu_H2 小时前
C++:构造函数与初始化列表详解
开发语言·c++·构造函数
落羽的落羽2 小时前
【Linux系统】总结线程:死锁问题、实现带有日志模块的线程池类
linux·运维·服务器·c++·人工智能·机器学习
琪露诺大湿2 小时前
VeloQueue-测试报告
java·开发语言·消息队列·单元测试·项目·测试报告
minji...2 小时前
Linux 网络套接字编程(四)支持多客户端同时在线、消息能转发给所有人的 UDP 聊天室服务器
linux·运维·开发语言·网络·c++·算法·udp
XS0301062 小时前
Java 基础(十一)反射
java·开发语言
t***5442 小时前
Dev-C++中使用Clang调试有哪些常见错误
java·开发语言·c++
郝学胜-神的一滴2 小时前
[简化版 GAMES 101] 计算机图形学 06:相机视图矩阵的由来
c++·线性代数·unity·矩阵·godot·图形渲染·unreal engine