第2章:C++内存模型概述

说到C++内存模型,我得先坦白一件事。我早年写多线程代码时,总觉得这东西离我很远。直到有一次,一个看似简单的计数器程序,在四核ARM设备上跑出了完全不符合预期的结果……嗯,从那以后,我老老实实把C++内存模型从头啃了一遍。

2.1 为什么需要内存模型?

说白了,内存模型就是一套规则。它告诉编译器、CPU、缓存系统:你对内存的操作,到底该怎么排序,怎么可见。

没有这套规则之前,我们靠的是平台相关的“黑魔法”。比如用volatile、用特定CPU的内存屏障指令。每个平台一套写法,移植起来简直要命。

C++11开始,标准委员会终于干了件漂亮事——把内存模型写进了语言标准。从此,我们写跨平台并发代码,有了统一的“法律依据”。

2.2 C++11/14/17/20 内存模型演进

我习惯把C++内存模型的演进分成四个阶段。每个阶段都解决了一些实际问题。

标准版本 核心改进 我印象最深的变化
C++11 引入完整内存模型、原子操作、happens-before 终于有标准化的atomic了
C++14 细化内存顺序定义,修复一些缺陷 memory_order_consume的坑开始被注意
C++17 强化对硬件一致性的要求 我在项目中遇到过,某些ARM芯片上的行为更明确了
C++20 信号量、latch、barrier等同步原语 写线程池终于不用自己造轮子了

核心观点:C++11是分水岭。之前的多线程代码,严格来说属于“未定义行为”的重灾区。之后,我们有了可靠的工具。

2.3 happens-before 关系

happens-before,你想想看,这名字其实挺直白的。就是“A操作一定在B操作之前发生”。但这里的“之前”,不是墙上时钟的之前,而是“逻辑上可见的之前”。

举个例子:

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

// 线程1
data = 42;           // A
flag.store(1, std::memory_order_release);  // B

// 线程2
while (flag.load(std::memory_order_acquire) != 1);  // C
int x = data;        // D

这里,A happens-before B,C happens-before D。而且因为B和C是release-acquire配对,所以A的效果对D是可见的。说白了,data=42这个赋值,在线程2里一定能读到。

我的经验:happens-before不是魔法。它依赖于你正确使用原子操作的内存顺序。我曾经见过有人用memory_order_relaxed做同步,结果数据乱得一塌糊涂。

2.4 sequentially consistent 原子操作

sequentially consistent(SC)是C++里最“强”的内存顺序。它保证:所有线程看到的原子操作顺序,都和某个全局顺序一致。

你想想看,这相当于给所有线程的原子操作排了个队。谁先谁后,一目了然。

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

// 线程1
x.store(1, std::memory_order_seq_cst);  // A

// 线程2
y.store(1, std::memory_order_seq_cst);  // B

// 线程3
int a = x.load(std::memory_order_seq_cst);  // C
int b = y.load(std::memory_order_seq_cst);  // D

// 线程4
int c = y.load(std::memory_order_seq_cst);  // E
int d = x.load(std::memory_order_seq_cst);  // F

在SC下,所有线程看到的A和B的顺序是一致的。要么A先B后,要么B先A后。不会出现线程3看到A先B后,线程4看到B先A后的情况。

注意:SC虽然安全,但性能开销最大。在x86上还好,因为x86的内存模型本身就比较强。但在ARM/PowerPC上,SC会生成额外的内存屏障指令。我曾在ARM设备上测过,SC比relaxed慢了将近3倍。

2.5 知识体系总览

下面这张图,是我自己总结的C++内存模型知识结构。每次给团队做培训,我都会先画一遍。

C++内存模型 标准演进 C++11: 基础框架 C++14: 细化定义 C++17: 硬件一致性 C++20: 同步原语 核心概念 happens-before 关系 内存顺序 (6种) 原子操作类型 数据竞争定义 实践应用 锁无关数据结构 跨平台同步 性能调优 调试与验证 核心目标:可预测的跨平台并发行为 避免未定义行为,保证代码在不同硬件上正确运行

2.6 避坑指南

我曾经在项目中犯过一个低级错误。用memory_order_relaxed做生产者-消费者的同步,结果在ARM设备上,消费者总是读到过期的数据。排查了两天才发现,relaxed不提供任何happens-before保证。

这里我总结几个常见坑:

  • 不要默认用relaxed——除非你清楚自己在做什么,并且能证明正确性
  • SC不是万能药——它保证顺序,但不保证实时性
  • happens-before是传递的——A happens-before B,B happens-before C,则A happens-before C。这个性质经常被忽略
  • volatile不等于atomic——volatile不提供任何线程间同步保证

我的建议:刚开始接触C++内存模型时,先用SC。等对happens-before关系有了直觉,再考虑用更弱的内存顺序来优化性能。别一上来就追求极致性能,正确性永远是第一位的。

好了,这一章的内容就到这里。C++内存模型是个大话题,我们后面还会反复提到。记住一点:理解happens-before,就抓住了内存模型的精髓。

可可精品资料库