在讨论编程问题,尤其是在并发编程领域时,我们经常会遇到「内存模型」这个词,其英文通常为 "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 代表存储在处理器寄存器中的变量NEW 代表一个非 0 常量(在本系列后续文章中,将沿用类似的表示方法,届时不再重复说明。)
现在,我们提出一个问题:
{{< alert >}}
程序运行后,(r1, r2) 的值是否可能为 (0, 0) ?
{{< /alert >}}
这个问题看似简单,通常的推理逻辑如下:
r1 == 0 则说明 y == 0y == 0 则说明 y = NEW 尚未执行r2 = x 更不可能执行(r1, r2) == (0, 0) 是不可能的这种推理非常符合直觉,但它是否正确呢?答案是——取决于处理器的「内存模型」。事实上,上述推理的成立需要特定内存模型的保证。我们最基础的直觉是「先执行的指令,其结果先被观察到」,若硬件层面能够保证这一点,程序员就能省去不少麻烦。然而,这也会限制硬件实现者的优化空间。
在某些内存模型下,(0, 0) 这个结果是可能出现的,因为它们允许对符合规则的指令进行重排,例如出现这样的执行序列:L1 → L2 → S1 → S2 。为什么部分内存模型会允许这样的顺序呢?主要是为了赋予硬件实现者更大的自由度,以进行更多性能优化(优化的具体原理不在本文讨论范围)。然而,一方的「自由」往往意味着另一方的「不自由」。程序员可能会感到世界观崩塌,仿佛一切皆不可信。但实际情况并非如此糟糕,因为指令重排并非随意进行,而是遵循特定规则的。当程序中某些操作必须保持顺序时,处理器也提供了相应的工具来保证这一点。
不过无论如何,我们需要学习规则,并使用工具,才能达到目的,编程确实变难了。
至此,我们对「内存模型」已有了初步的直观认识。对于以下类型的程序:
我们自然会提出:
定义并澄清上述问题的规则,就构成了所谓的「内存模型」。它为 CPU 设计者、编译器开发者、操作系统工程师以及应用程序员等不同角色提供了统一的协作框架。
从下一篇文章开始,我们将逐一介绍几种主流的内存模型。