c++内存模型与原子操作

Post Cover

C++ 内存模型与原子操作详解

一、为什么需要内存模型?

现代硬件和编译器为了性能会对指令进行重排序(Reordering)

  • 编译器重排:编译器在不改变单线程语义的前提下,可以任意重排指令顺序。
  • CPU 乱序执行:CPU 可能以不同于程序顺序的方式执行指令(Out-of-Order Execution)。
  • Store Buffer / 写缓冲区:CPU 的写操作不一定立即对其他核可见。
  • Cache 一致性延迟:多核之间的缓存同步存在延迟。

这些优化在单线程下透明,但在多线程下会导致数据竞争(Data Race)和不可预期的行为。C++11 内存模型为此提供了一套跨平台的同步原语。


二、std::atomic 原子类型

std::atomic<T> 保证对类型 T 的操作是原子的(不可分割),不会被其他线程中途观察到中间状态。

1
2
3
4
5
6
7
8
9
10
11
12
#include <atomic>

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

// 原子自增
counter.fetch_add(1, std::memory_order_relaxed);

// 原子读
int val = counter.load(std::memory_order_acquire);

// 原子写
counter.store(42, std::memory_order_release);

注意atomic 只保证操作本身的原子性,线程间的可见性和有序性由内存序(Memory Order)控制。


三、六种内存序(Memory Order)

C++ 提供了 6 种内存序,定义在 <atomic> 头文件中:

枚举值 含义
memory_order_relaxed 无同步约束,仅保证原子性
memory_order_consume 依赖链上的加载有序(已基本废弃,慎用)
memory_order_acquire 获取语义,防止后续读写被提前
memory_order_release 释放语义,防止前面读写被延后
memory_order_acq_rel 获取+释放,用于 RMW 操作
memory_order_seq_cst 顺序一致,最强保证(默认)

3.1 Relaxed — 仅保证原子性

1
2
3
std::atomic<int> x{0};
x.store(1, std::memory_order_relaxed);
int v = x.load(std::memory_order_relaxed);
  • 不建立任何 happens-before 关系。
  • 适用场景:计数器统计,不关心顺序,只关心最终值。
  • 性能最高,但使用不当极易出 bug。

3.2 Release / Acquire — 释放-获取语义

这是最常用的同步对,构成线程间通信的基础。

规则:

  • store(..., release) 保证:该操作之前的所有写入,对随后执行 load(..., acquire) 并读到该值的线程可见。
  • load(..., acquire) 保证:该操作之后的所有读写,不会被重排到 load 之前。
1
2
3
4
5
6
7
8
9
10
std::atomic<bool> ready{false};
int data = 0;

// 线程 1(生产者)
data = 42; // ① 写数据
ready.store(true, std::memory_order_release); // ② 发布信号

// 线程 2(消费者)
while (!ready.load(std::memory_order_acquire)); // ③ 等待信号
assert(data == 42); // ④ 安全读取,保证看到 ①

内存屏障视角:

1
2
3
线程1:  [写 data] → [store release]
↑ 同步点
线程2: [load acquire] → [读 data]

3.3 Acquire-Release (acq_rel) — 用于 RMW 操作

fetch_addcompare_exchange 等**读-改-写(Read-Modify-Write)**操作可以使用 acq_rel,同时具备 acquire 和 release 语义。

1
2
3
4
5
6
7
8
9
std::atomic<int> lock{0};

// 尝试加锁(acquire)
while (lock.exchange(1, std::memory_order_acquire) == 1);

// 临界区 ...

// 释放锁(release)
lock.store(0, std::memory_order_release);

3.4 Sequential Consistent — 顺序一致

1
2
3
std::atomic<int> x{0}, y{0};
x.store(1, std::memory_order_seq_cst);
y.store(1, std::memory_order_seq_cst);
  • 所有 seq_cst 操作存在一个全局一致的全序(Total Order),所有线程观察到的操作顺序相同。
  • 最容易推理,但开销最大(在 x86 上几乎无额外开销,在 ARM/POWER 上开销显著)。
  • 默认的内存序atomic.store(val) 等价于 atomic.store(val, memory_order_seq_cst)

四、Happens-Before 关系

Happens-Before 是 C++ 内存模型的核心概念,定义了操作间的可见性保证

1
A happens-before B  →  A 的效果对 B 可见

建立 happens-before 的方式:

  1. 同一线程内:按程序顺序,前面的操作 happens-before 后面的操作。
  2. Release-Acquire 配对store(release) synchronizes-with load(acquire)(读到该 store 的值),从而建立 happens-before。
  3. 互斥锁mutex.unlock() happens-before mutex.lock()(后续成功加锁)。
  4. 线程创建/join:父线程的创建操作 happens-before 子线程的起始;子线程的结束 happens-before join() 返回。

五、CAS —— Compare-And-Swap

5.1 基本概念

CAS 是无锁编程的基石,语义为:

1
2
3
4
5
6
7
if (*addr == expected) {
*addr = desired;
return true;
} else {
expected = *addr; // 更新 expected 为当前值
return false;
}

C++ 提供两个版本:

1
2
3
4
5
6
7
8
9
// 强版本:只在实际不等时返回 false
bool compare_exchange_strong(T& expected, T desired,
std::memory_order success,
std::memory_order failure);

// 弱版本:允许伪失败(spurious failure),但在循环中性能更好
bool compare_exchange_weak(T& expected, T desired,
std::memory_order success,
std::memory_order failure);

5.2 CAS 循环模式

1
2
3
4
5
6
7
8
9
10
11
12
std::atomic<int> value{0};

// 无锁自增示例
int old_val = value.load(std::memory_order_relaxed);
while (!value.compare_exchange_weak(
old_val,
old_val + 1,
std::memory_order_release,
std::memory_order_relaxed))
{
// old_val 已被自动更新为当前值,重试
}

5.3 success 与 failure 内存序

参数 含义
success CAS 成功时(执行了写入),使用的内存序
failure CAS 失败时(只做了读取),使用的内存序

规则: failure 的内存序不能强于 success,且 failure 不能是 releaseacq_rel(因为失败时没有写入)。

5.4 ABA 问题

CAS 的经典陷阱:

1
2
3
线程1 读到 A
线程2 将 A → B → A(值变了又变回来)
线程1 CAS(A, new) ← 成功,但逻辑上数据已经被修改过了!

解决方案:

  • 使用带版本号的 CAS(Tagged Pointer):将值和版本号打包在一个 64 位整数中。
  • 使用 std::atomic<std::shared_ptr<T>> 等高层抽象。
  • 在 GC 语言中天然避免(C++ 需要手动处理)。
1
2
3
4
5
6
// 带版本号的 CAS 示例(128-bit CAS,部分平台支持)
struct TaggedPtr {
void* ptr;
uintptr_t tag;
};
std::atomic<TaggedPtr> head;

六、内存屏障(Memory Fence / Barrier)

除了原子操作自带的内存序,C++ 还提供独立的 fence

1
2
3
4
5
#include <atomic>

std::atomic_thread_fence(std::memory_order_release); // 释放屏障
std::atomic_thread_fence(std::memory_order_acquire); // 获取屏障
std::atomic_thread_fence(std::memory_order_seq_cst); // 全屏障

fence 比操作自带的内存序更”重”,会对 fence 前后的所有原子操作生效:

1
2
3
4
// 效果:store 之前的所有写入对后续 acquire-load 可见
data = 42;
std::atomic_thread_fence(std::memory_order_release);
flag.store(true, std::memory_order_relaxed);

七、常见无锁数据结构模式

7.1 无锁栈(Treiber Stack)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head{nullptr};

public:
void push(T val) {
Node* node = new Node{val, nullptr};
node->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(node->next, node,
std::memory_order_release,
std::memory_order_relaxed));
}

bool pop(T& out) {
Node* old_head = head.load(std::memory_order_acquire);
while (old_head) {
if (head.compare_exchange_weak(old_head, old_head->next,
std::memory_order_acquire,
std::memory_order_relaxed)) {
out = old_head->data;
delete old_head;
return true;
}
}
return false;
}
};

7.2 双重检查锁(DCLP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::atomic<Singleton*> instance{nullptr};
std::mutex mtx;

Singleton* get_instance() {
Singleton* p = instance.load(std::memory_order_acquire);
if (!p) {
std::lock_guard<std::mutex> lock(mtx);
p = instance.load(std::memory_order_relaxed);
if (!p) {
p = new Singleton();
instance.store(p, std::memory_order_release);
}
}
return p;
}

八、各内存序的硬件映射

C++ 内存序 x86-64 指令 ARM 指令
relaxed load/store MOV LDR / STR
acquire load MOV(x86 自带 acquire 语义) LDAR
release store MOV(x86 自带 release 语义) STLR
seq_cst store MFENCE + MOV / XCHG STLR + DMB
seq_cst load MOV LDAR

x86 的强内存模型:x86 硬件天然保证 store-load 之外的内存序,因此 acquire/release 在 x86 上几乎零开销。ARM 是弱内存模型,需要显式屏障指令。


九、总结与选型建议

1
2
性能高 ←————————————————————————→ 安全性高
relaxed consume acquire/release seq_cst
场景 推荐内存序
无顺序依赖的计数器(如统计) relaxed
生产者-消费者、发布-订阅 release + acquire
无锁队列、栈的 RMW acq_rel
需要全局一致顺序(如 Dekker 算法) seq_cst
不确定时 seq_cst(正确性优先)

黄金法则:

  1. 优先使用高层同步原语(mutexcondition_variable)。
  2. 需要无锁时,先用 seq_cst,确认正确后再优化内存序。
  3. 任何 relaxed 的使用都需要详细的注释说明为什么安全。
  4. 警惕 ABA 问题和内存回收问题(使用 Hazard Pointer 或 RCU)。# C++ 内存模型与原子操作详解

一、为什么需要内存模型?

现代硬件和编译器为了性能会对指令进行重排序(Reordering)

  • 编译器重排:编译器在不改变单线程语义的前提下,可以任意重排指令顺序。
  • CPU 乱序执行:CPU 可能以不同于程序顺序的方式执行指令(Out-of-Order Execution)。
  • Store Buffer / 写缓冲区:CPU 的写操作不一定立即对其他核可见。
  • Cache 一致性延迟:多核之间的缓存同步存在延迟。

这些优化在单线程下透明,但在多线程下会导致数据竞争(Data Race)和不可预期的行为。C++11 内存模型为此提供了一套跨平台的同步原语。


二、std::atomic 原子类型

std::atomic<T> 保证对类型 T 的操作是原子的(不可分割),不会被其他线程中途观察到中间状态。

1
2
3
4
5
6
7
8
9
10
11
12
#include <atomic>

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

// 原子自增
counter.fetch_add(1, std::memory_order_relaxed);

// 原子读
int val = counter.load(std::memory_order_acquire);

// 原子写
counter.store(42, std::memory_order_release);

注意atomic 只保证操作本身的原子性,线程间的可见性和有序性由内存序(Memory Order)控制。


三、六种内存序(Memory Order)

C++ 提供了 6 种内存序,定义在 <atomic> 头文件中:

枚举值 含义
memory_order_relaxed 无同步约束,仅保证原子性
memory_order_consume 依赖链上的加载有序(已基本废弃,慎用)
memory_order_acquire 获取语义,防止后续读写被提前
memory_order_release 释放语义,防止前面读写被延后
memory_order_acq_rel 获取+释放,用于 RMW 操作
memory_order_seq_cst 顺序一致,最强保证(默认)

3.1 Relaxed — 仅保证原子性

1
2
3
std::atomic<int> x{0};
x.store(1, std::memory_order_relaxed);
int v = x.load(std::memory_order_relaxed);
  • 不建立任何 happens-before 关系。
  • 适用场景:计数器统计,不关心顺序,只关心最终值。
  • 性能最高,但使用不当极易出 bug。

3.2 Release / Acquire — 释放-获取语义

这是最常用的同步对,构成线程间通信的基础。

规则:

  • store(..., release) 保证:该操作之前的所有写入,对随后执行 load(..., acquire) 并读到该值的线程可见。
  • load(..., acquire) 保证:该操作之后的所有读写,不会被重排到 load 之前。
1
2
3
4
5
6
7
8
9
10
std::atomic<bool> ready{false};
int data = 0;

// 线程 1(生产者)
data = 42; // ① 写数据
ready.store(true, std::memory_order_release); // ② 发布信号

// 线程 2(消费者)
while (!ready.load(std::memory_order_acquire)); // ③ 等待信号
assert(data == 42); // ④ 安全读取,保证看到 ①

内存屏障视角:

1
2
3
线程1:  [写 data] → [store release]
↑ 同步点
线程2: [load acquire] → [读 data]

3.3 Acquire-Release (acq_rel) — 用于 RMW 操作

fetch_addcompare_exchange 等**读-改-写(Read-Modify-Write)**操作可以使用 acq_rel,同时具备 acquire 和 release 语义。

1
2
3
4
5
6
7
8
9
std::atomic<int> lock{0};

// 尝试加锁(acquire)
while (lock.exchange(1, std::memory_order_acquire) == 1);

// 临界区 ...

// 释放锁(release)
lock.store(0, std::memory_order_release);

3.4 Sequential Consistent — 顺序一致

1
2
3
std::atomic<int> x{0}, y{0};
x.store(1, std::memory_order_seq_cst);
y.store(1, std::memory_order_seq_cst);
  • 所有 seq_cst 操作存在一个全局一致的全序(Total Order),所有线程观察到的操作顺序相同。
  • 最容易推理,但开销最大(在 x86 上几乎无额外开销,在 ARM/POWER 上开销显著)。
  • 默认的内存序atomic.store(val) 等价于 atomic.store(val, memory_order_seq_cst)

四、Happens-Before 关系

Happens-Before 是 C++ 内存模型的核心概念,定义了操作间的可见性保证

1
A happens-before B  →  A 的效果对 B 可见

建立 happens-before 的方式:

  1. 同一线程内:按程序顺序,前面的操作 happens-before 后面的操作。
  2. Release-Acquire 配对store(release) synchronizes-with load(acquire)(读到该 store 的值),从而建立 happens-before。
  3. 互斥锁mutex.unlock() happens-before mutex.lock()(后续成功加锁)。
  4. 线程创建/join:父线程的创建操作 happens-before 子线程的起始;子线程的结束 happens-before join() 返回。

五、CAS —— Compare-And-Swap

5.1 基本概念

CAS 是无锁编程的基石,语义为:

1
2
3
4
5
6
7
if (*addr == expected) {
*addr = desired;
return true;
} else {
expected = *addr; // 更新 expected 为当前值
return false;
}

C++ 提供两个版本:

1
2
3
4
5
6
7
8
9
// 强版本:只在实际不等时返回 false
bool compare_exchange_strong(T& expected, T desired,
std::memory_order success,
std::memory_order failure);

// 弱版本:允许伪失败(spurious failure),但在循环中性能更好
bool compare_exchange_weak(T& expected, T desired,
std::memory_order success,
std::memory_order failure);

5.2 CAS 循环模式

1
2
3
4
5
6
7
8
9
10
11
12
std::atomic<int> value{0};

// 无锁自增示例
int old_val = value.load(std::memory_order_relaxed);
while (!value.compare_exchange_weak(
old_val,
old_val + 1,
std::memory_order_release,
std::memory_order_relaxed))
{
// old_val 已被自动更新为当前值,重试
}

5.3 success 与 failure 内存序

参数 含义
success CAS 成功时(执行了写入),使用的内存序
failure CAS 失败时(只做了读取),使用的内存序

规则: failure 的内存序不能强于 success,且 failure 不能是 releaseacq_rel(因为失败时没有写入)。

5.4 ABA 问题

CAS 的经典陷阱:

1
2
3
线程1 读到 A
线程2 将 A → B → A(值变了又变回来)
线程1 CAS(A, new) ← 成功,但逻辑上数据已经被修改过了!

解决方案:

  • 使用带版本号的 CAS(Tagged Pointer):将值和版本号打包在一个 64 位整数中。
  • 使用 std::atomic<std::shared_ptr<T>> 等高层抽象。
  • 在 GC 语言中天然避免(C++ 需要手动处理)。
1
2
3
4
5
6
// 带版本号的 CAS 示例(128-bit CAS,部分平台支持)
struct TaggedPtr {
void* ptr;
uintptr_t tag;
};
std::atomic<TaggedPtr> head;

六、内存屏障(Memory Fence / Barrier)

除了原子操作自带的内存序,C++ 还提供独立的 fence

1
2
3
4
5
#include <atomic>

std::atomic_thread_fence(std::memory_order_release); // 释放屏障
std::atomic_thread_fence(std::memory_order_acquire); // 获取屏障
std::atomic_thread_fence(std::memory_order_seq_cst); // 全屏障

fence 比操作自带的内存序更”重”,会对 fence 前后的所有原子操作生效:

1
2
3
4
// 效果:store 之前的所有写入对后续 acquire-load 可见
data = 42;
std::atomic_thread_fence(std::memory_order_release);
flag.store(true, std::memory_order_relaxed);

七、常见无锁数据结构模式

7.1 无锁栈(Treiber Stack)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
template<typename T>
class LockFreeStack {
struct Node {
T data;
Node* next;
};
std::atomic<Node*> head{nullptr};

public:
void push(T val) {
Node* node = new Node{val, nullptr};
node->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(node->next, node,
std::memory_order_release,
std::memory_order_relaxed));
}

bool pop(T& out) {
Node* old_head = head.load(std::memory_order_acquire);
while (old_head) {
if (head.compare_exchange_weak(old_head, old_head->next,
std::memory_order_acquire,
std::memory_order_relaxed)) {
out = old_head->data;
delete old_head;
return true;
}
}
return false;
}
};

7.2 双重检查锁(DCLP)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::atomic<Singleton*> instance{nullptr};
std::mutex mtx;

Singleton* get_instance() {
Singleton* p = instance.load(std::memory_order_acquire);
if (!p) {
std::lock_guard<std::mutex> lock(mtx);
p = instance.load(std::memory_order_relaxed);
if (!p) {
p = new Singleton();
instance.store(p, std::memory_order_release);
}
}
return p;
}

八、各内存序的硬件映射

C++ 内存序 x86-64 指令 ARM 指令
relaxed load/store MOV LDR / STR
acquire load MOV(x86 自带 acquire 语义) LDAR
release store MOV(x86 自带 release 语义) STLR
seq_cst store MFENCE + MOV / XCHG STLR + DMB
seq_cst load MOV LDAR

x86 的强内存模型:x86 硬件天然保证 store-load 之外的内存序,因此 acquire/release 在 x86 上几乎零开销。ARM 是弱内存模型,需要显式屏障指令。


九、总结与选型建议

1
2
性能高 ←————————————————————————→ 安全性高
relaxed consume acquire/release seq_cst
场景 推荐内存序
无顺序依赖的计数器(如统计) relaxed
生产者-消费者、发布-订阅 release + acquire
无锁队列、栈的 RMW acq_rel
需要全局一致顺序(如 Dekker 算法) seq_cst
不确定时 seq_cst(正确性优先)

黄金法则:

  1. 优先使用高层同步原语(mutexcondition_variable)。
  2. 需要无锁时,先用 seq_cst,确认正确后再优化内存序。
  3. 任何 relaxed 的使用都需要详细的注释说明为什么安全。
  4. 警惕 ABA 问题和内存回收问题(使用 Hazard Pointer 或 RCU)。

本作品由 lorixyu 于 2026-03-18 16:22:16 发布
作品地址:c++内存模型与原子操作
除特别声明外,本站作品均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自 lorixyu
Logo