【内存模型】什么是内存模型
讨论编程问题的时候,尤其是在并发编程的领域,我们经常看到会「内存模型」这个词,英语通常叫「Memory Model」或「Memory Consistency Model」。理解「内存模型」究竟意味着什么,对于写出高效稳定的计算机程序至关重要。但是,理解「内存模型」并不是一件容易的事情。本系列文章的目的,希望能对「内存模型」这个概念,给出浅显易懂的解释。尤其是帮助在嵌入式开发领域的软件工程师理解、掌握工作中必须要用到的知识。
本篇我们先从一个简单的问题开始,介绍内存模型的概念。
// Core C1
S1: x = NEW;
L1: r1 = y;
// Core C2
S2: y = NEW;
L2: r2 = x;
上面的代码采用了类似 C 语言的伪代码,呈现了在两个处理器核上运行的一些指令。对这里的一些符号和初始条件做一些说明:
C1
、C2
代表处理器核(Core)L1
、L2
代表读指令(Load)S1
、S2
代表写指令(Store)x
、y
代表存储在共享内存中的变量r1
、r2
代表存储在处理器寄存器中的变量- 所有变量的初始值都是 0
NEW
代表一个非 0 常量
(在系列的后面文章中,都会用类似的表示方法,出现时将不再对此赘述。)
问题来了:
(r1, r2)
的值可能是 (0, 0)
吗?这个问题看似很简单,一般的推理是这样的:
r1 == 0
意味着y == 0
y == 0
意味着y = NEW
尚未执行- 所以
r2 = x
更不可能执行 - 所以
(r1, r2) == (0, 0)
是不可能的
这种推理是最符合直觉的,不过,它是正确的吗?答案是——取决于处理器的「内存模型」。上面的推理,其实是需要某种内存模型来保证的。我们最基本的直觉是——先执行的指令,其结果先「被看到」,在硬件层面保证这种关系是最简单的,写代码的人就可以少操心。但是,硬件实现者的手被束缚了。
另一些内存模型下,(0, 0)
这个结果是可能出现的,因为部分内存模型允许重排符合规则的指令,比如这个执行序列:L1 → L2 → S1 → S2
。为什么部分内存模型会允许这样的执行序列呢?主要是为了让硬件的实现者有更大自由度,做更多的优化(优化的原理不是本文重点)。有人自由了,就有人会不自由。程序员感觉世界崩塌了,什么都不可信了。其实,没有完全崩塌,因为指令的重排并不是随机的,而是有一定规则的,并且,当我们识别到有程序需要保证先后顺序的时候,处理器也给了我们工具来实现。
不过无论如何,我们需要学习规则,并使用工具,才能达到目的,编程确实变难了。
到这里,我们对「内存模型」这个概念,已经有了一定的直观认识。对于这样一类程序:
- 运行在某种 CPU 指令集上
- 在多个 CPU 上执行多个线程
- 多个线程对共享内存进行读写
我们会问:
- 硬件的实现者可以对这些读写指令做哪些优化?规则是什么?
- 软件的实现者可以信任哪些假设?不能信任哪些假设?如何来保证执行顺序?
澄清以上问题的规则,构成了「内存模型」。「内存模型」让 CPU 设计者、编译器实现者、操作系统开发者、应用软件开发者等角色在统一的规则下工作。
从下一篇开始,我们会逐个介绍几种主流的内存模型。