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::mutex的lock()/unlock()std::atomic的各种操作std::thread::join()std::condition_variable的wait()/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_add、compare_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 是默认内存序,提供最强的保证:
- 包含 release 和 acquire 的所有保证
- 额外保证:所有 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 会生成 MFENCE 或 LOCK 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::mutex、std::condition_variable、std::shared_ptr 已经处理好了内存序,不需要手动考虑。只有在对性能要求极高的热路径才考虑手写 atomic。
原则4:先写正确,再优化内存序。 默认用 seq_cst(atomic 的默认值),验证正确后再分析是否可以放宽到 acquire/release 或 relaxed。
原则5:用 TSan 验证。 人工检查内存模型正确性极易出错,ThreadSanitizer 是必备工具。