本文由 Russ Cox 大佬撰写,发布于 2021年6月29日,原文链接 Hardware Memory Models。博主是读了此文后才恍然理解 C++ 中的 Memory Order 的技术背景和设计动机。特此翻译留念。
LLM 使用声明:本文翻译重度依赖 LLM 技术。
Table of Contents
引言:好时代,结束力
很久以前,在那个单线程程序横行的年代,让程序跑得更快最有效的方法居然是——什么都不做。下一代硬件的优化和新一代编译器的魔法,能让你现有的程序行为丝毫不差,只是跑得更快。在那个美好的童话时代,判断一项优化是否有效的标准简单明了:如果程序员分辨不出优化前后的差异(除了速度变快),那这项优化就是有效的。换句话说,有效的优化绝不会改变正确程序的行为。
可惜好景不长。多年前,硬件工程师们让单核处理器越来越快的魔法突然失灵了。于是他们施展了新的法术——制造出拥有越来越多核心的处理器,操作系统则将这些硬件并行能力包装成"线程"的概念呈现给程序员。这个新的魔法(多处理器+操作系统线程)对硬件工程师来说确实很管用,却给语言设计者、编译器开发者和程序员带来了无尽的烦恼。
许多在单线程世界里悄无声息的硬件和编译器优化,在多线程环境中露出了獠牙,变得肉眼可见。既然有效的优化不该改变正确程序的行为,那么这些优化要么被宣判为无效,要么就得重新定义什么是"正确程序"。何去何从?我们又该如何决断?
让我们先看一个类C语言的简单例子。本文所有示例中,所有变量默认初始化为零。
// 线程 1 // 线程 2
x = 1; while(done == 0) { /* 空转 */ }
done = 1; print(x);
如果线程1和线程2各跑在一个专属核心上,且都执行完毕,这个程序有没有可能打印出0?
答案是——看情况。它既取决于硬件平台,也取决于编译器实现。在x86多核处理器上逐字翻译成汇编代码,永远会打印1。但在ARM或POWER多核处理器上做同样的翻译,却可能打印出0。更糟的是,不管底层硬件如何,标准编译器优化都可能让这个程序要么打印0,要么陷入死循环。
"看情况"——这可不是程序员想听到的答案。程序员需要确切的保证:我的程序在新硬件、新编译器下还能不能正常工作?硬件设计者和编译器开发者也需要明确的规范:硬件和生成的代码到底可以有多"放飞自我"?由于问题的核心在于内存数据变化的可见性和一致性,这份"君子协定"就被称为内存一致性模型,简称内存模型。
早期的内存模型只关心硬件对汇编程序员的承诺,编译器根本不在考虑范围内。大约25年前,人们开始为Java、C++等高级语言设计内存模型,试图明确这些语言对程序员的承诺。把编译器这尊大神也请进来,让定义一个靠谱的内存模型变得难上加难。
本文是探讨硬件内存模型和编程语言内存模型的系列文章的第一篇。我写这个系列的目的,是为讨论Go语言内存模型可能的改进做铺垫。但要想搞清楚Go的现状和未来方向,我们必须先弄明白其他硬件和语言的内存模型现在长什么样,以及它们是怎么一步步走到今天这个鬼样子的。
需要强调的是,本文只谈硬件。假设我们在为多核处理器写汇编代码,程序员究竟需要硬件做出哪些保证才能写出正确的程序?计算机科学家们为这个问题的答案苦苦追寻了四十多年。
顺序一致性:理想国
1979年,Leslie Lamport在论文《如何构建能正确执行多进程程序的多处理器》中抛出了顺序一致性这个概念:
在为多处理器设计和证明多进程算法正确性时,我们通常假设满足以下条件:任何执行结果都等同于将所有处理器的操作按某种顺序串行执行的结果,且每个处理器自身的操作严格按照程序顺序出现。满足此条件的多处理器称为顺序一致。
如今我们不仅谈硬件的顺序一致性,也谈编程语言的顺序一致性——程序的所有可能执行都必须对应某种线程操作的交错序列。顺序一致性被视为最理想的模型,对程序员最直观的模型。它让你可以天真地认为程序会按代码顺序执行,各线程只是简单交错,不会被随意重组。
(有人质疑顺序一致性是否真的应该当老大,不过这超出了本文的讨论范围。)
回到开头的例子,为了使这个litmus测试更容易分析,让我们去掉循环和打印语句,只询问关于读取共享变量的可能结果:
Litmus测试:消息传递
这个程序能否看到 r1 = 1
, r2 = 0
?
// 线程 1 // 线程 2
x = 1 r1 = y
y = 1 r2 = x
我们假设每个例子开始时所有共享变量都设置为零。因为我们试图确定硬件被允许做什么,我们假设每个线程在自己的专用处理器上执行,并且没有编译器重新排序线程中发生的事情:列表中的指令就是处理器执行的指令。名称 rN
表示线程局部寄存器,不是共享变量,我们询问在程序执行结束时线程局部寄存器的特定设置是否可能。
这种关于示例程序执行结果的问题被称为litmus测试。因为它有一个二元答案——这个结果是否可能?——litmus测试为我们提供了一种清晰的方式来区分内存模型:如果一个模型允许特定的执行而另一个模型不允许,那么这两个模型显然是不同的。不幸的是,正如我们稍后将会看到的,特定模型对特定litmus测试给出的答案常常是令人惊讶的。
如果这个litmus测试的执行是顺序一致的,那么只有六种可能的交错,由于没有哪种交错会以 r1 = 1
, r2 = 0
结束,这个结果是不被允许的。也就是说,在顺序一致的硬件上,对这个litmus测试的答案——这个程序能否看到 r1 = 1
, r2 = 0
?——是不能。

顺序一致性的一个很好的心智模型是想象所有处理器直接连接到同一个共享内存,这个内存一次只能为一个线程提供读或写请求。这里没有涉及缓存,所以每当处理器需要从内存读取或写入内存时,该请求都会发送到共享内存。一次只能使用一次的共享内存在所有内存访问的执行上施加了一个顺序:顺序一致性。

这个图是一个顺序一致机器的模型,不是构建这种机器的唯一方式。确实,可以使用多个共享内存模块和缓存来构建顺序一致的机器,这些缓存有助于预测内存获取的结果,但成为顺序一致意味着机器必须表现得与这个模型无法区分。如果我们只是试图理解顺序一致执行意味着什么,我们可以忽略所有这些可能的实现复杂性,只考虑这一个模型。
不幸的是,对于我们作为程序员来说,放弃严格的顺序一致性可以让硬件更快地执行程序,所以所有现代硬件都以各种方式偏离了顺序一致性。精确定义特定硬件如何偏离结果变得非常困难。这篇文章以今天广泛使用的硬件中的两个内存模型为例:x86的内存模型,以及ARM和POWER处理器系列的内存模型。
x86总存储顺序(x86-TSO)
现代x86系统的内存模型对应于这样的硬件设计:所有处理器仍然连接到单个共享内存,但每个处理器将对该内存的写入排入本地写队列中。处理器继续执行新指令,而写入则慢慢传播到共享内存。一个处理器上的内存读取会首先查询本地写队列,然后再查询主内存,但它看不到其他处理器上的写队列。结果是处理器看到自己的写入比别人早。但是——这非常重要——所有处理器都同意写入(存储)到达共享内存的(总)顺序,因此这个模型得名:总存储顺序,或TSO。在写入到达共享内存的那一刻,任何处理器上的任何未来读取都将看到它并使用该值(直到它被后来的写覆盖,或者可能被来自另一个处理器的缓冲写覆盖)。

写队列是一个标准的先进先出队列:内存写入以处理器执行它们的相同顺序应用于共享内存。因为写顺序由写队列保持,并且因为其他处理器立即看到对共享内存的写入,我们之前考虑的消息传递litmus测试具有与之前相同的结果:r1 = 1
, r2 = 0
仍然是不可能的。
Litmus测试:消息传递
这个程序能否看到 r1 = 1
, r2 = 0
?
// 线程 1 // 线程 2
x = 1 r1 = y
y = 1 r2 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:否。
写队列保证线程1在写入 y
之前先将 x
写入内存,而系统范围内关于内存写入顺序的协议(总存储顺序)保证线程2在得知 x
的新值之前先得知 y
的新值。因此 r1 = y
看到新的 y
而 r2 = x
却没有看到新的 x
是不可能的。存储顺序在这里至关重要:线程1在写入 y
之前先写入 x
,所以线程2不得在在写入 x
之前看到对 y
的写入。
顺序一致性和TSO模型在这种情况下是一致的,但它们在其它litmus测试的结果上存在分歧。例如,这是通常区分这两个模型的例子:
Litmus测试:写队列(也称为存储缓冲器)
这个程序能否看到 r1 = 0
, r2 = 0
?
// 线程 1 // 线程 2
x = 1 y = 1
r1 = y r2 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:是的!
在任何顺序一致的执行中,要么 x = 1
要么 y = 1
必须首先发生,然后另一个线程中的读取必须观察到它,所以 r1 = 0
, r2 = 0
是不可能的。但在TSO系统上,线程1和线程2都可能将它们的写入排队,然后在任何一个写入到达内存之前从内存读取,这样两个读取都看到零。
这个例子可能看起来不自然,但使用两个同步变量的确出现在知名的同步算法中,如Dekker算法或Peterson算法,以及特设的解决方案中。如果一个线程不能看到来自另一个线程的所有写入,它们就会失效。
为了修复依赖于更强内存排序的算法,非顺序一致的硬件提供显式指令称为内存屏障(或栅栏),可用于控制排序。我们可以添加一个内存屏障来确保每个线程在开始读取之前将其之前的写入刷新到内存:
// 线程 1 // 线程 2
x = 1 y = 1
barrier barrier
r1 = y r2 = x
通过添加屏障,r1 = 0
, r2 = 0
再次变得不可能,Dekker或Peterson算法将正确工作。有许多种类的屏障;细节因系统而异,超出了这篇文章的范围。重点是屏障存在,并为程序员或语言实现者提供在程序的关键时刻强制顺序一致行为的方法。
最后一个例子,来说明为什么这个模型被称为总存储顺序。在模型中,有本地写队列但在读取路径上没有缓存。一旦写入到达主内存,所有处理器不仅同意值在那里,而且就它相对于来自其他处理器的写入何时到达达成一致。考虑这个litmus测试:
Litmus测试:独立写入的独立读取(IRIW)
这个程序能否看到 r1 = 1
, r2 = 0
, r1 = 1
, r4 = 0
?(线程3和4能否以不同顺序看到 x
和 y
的变化?)
// 线程 1 // 线程 2 // 线程 3 // 线程 4
x = 1 y = 1 r1 = x r3 = y
r2 = y r4 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:否。
如果线程3看到 x
在 y
之前变化,线程4能否看到 y
在 x
之前变化?对于x86和其他TSO机器,答案是否定的:对所有存储(写入)到主内存存在一个总顺序,所有处理器都同意这个顺序,前提是每个处理器都知道自己的写入在它们到达主内存之前。
x86-TSO的道路
x86-TSO模型看起来相当干净,但通往它的道路充满了障碍和错误的转弯。在1990年代,对第一批x86多处理器可用的手册几乎没有说明硬件提供的内存模型。
作为一个例子,Plan 9是最早的真正多处理器操作系统之一(没有全局内核锁)在x86上运行。在1997年移植到多处理器Pentium Pro期间,开发者遇到了意外行为,归根结底是写队列litmus测试。一段微妙的同步代码假设r1 = 0
, r2 = 0
是不可能的,然而它确实发生了。更糟糕的是,Intel手册对内存模型的细节说得很少。
在一次邮件列表讨论中,有人建议"比起相信硬件设计师会按我们期望的那样做,使用锁时采取保守态度更好",其中一位Plan 9开发者很好地解释了这个问题:
我当然同意。我们将遇到多处理器中更宽松的排序。问题是,硬件设计师认为什么是保守的?在锁定部分的开始和结束处强制进行互锁对我来说似乎相当保守,但我显然没有足够的想象力。Pro手册详细描述了缓存以及保持它们一致性的方法,但似乎不关心详细说明执行或读取排序。事实是我们没有办法知道我们是否足够保守。
在讨论过程中,一位Intel架构师非正式地解释了内存模型,指出理论上甚至多处理器486和Pentium系统都可能产生 r1 = 0
, r2 = 0
的结果,而Pentium Pro只是有更大的流水线和写队列,更频繁地暴露了这种行为。
这位Intel架构师还写道:
大致上说,这意味着系统中任何单个处理器产生的事件的排序,当其他处理器观察时,总是相同的。然而,不同的观察者被允许对来自两个或更多处理器的事件交错有不同的看法。
未来的Intel处理器将实现相同的内存排序模型。
声称"不同的观察者被允许对来自两个或更多处理器的事件交错有不同的看法"是在说x86对IRIW litmus测试的答案可以是"是",尽管我们在前一节看到x86回答"否"。这怎么可能?
答案似乎是Intel处理器实际上从未对该litmus测试回答过"是",但当时Intel架构师不愿意对未来处理器做出任何保证。架构手册中几乎不存在什么文本,使得针对它进行编程变得非常困难。
Plan 9的讨论并不是孤立事件。Linux内核开发者在他们的邮件列表上花费了超过一百条消息从1999年11月底开始,对Intel处理器提供的保证感到类似的困惑。
随着越来越多的人在这十年间遇到这些困难,Intel的一群架构师承担了编写关于处理器行为的有用保证的任务,既针对当前也针对未来处理器。第一个结果是"Intel 64架构内存排序白皮书",发表于2007年8月,旨在"为软件编写者提供对不同序列的内存访问指令可能产生的结果的清晰理解。" AMD在当年晚些时候在"AMD64架构程序员手册修订版3.14"中发表了类似的描述。这些描述基于一个称为"总锁顺序+因果一致性"(TLO+CC)的模型,有意比TSO更弱。在公开演讲中,Intel架构师表示TLO+CC是"尽可能强但不再更强。" 特别是,该模型保留了x86处理器对IRIW litmus测试回答"是"的权利。不幸的是,内存屏障的定义不够强大,无法重建顺序一致的内存语义,即使在每条指令后都有屏障。更糟的是,研究人员观察到实际的Intel x86硬件违反了TLO+CC模型。
为了应对这些问题,Owens等人提出了x86-TSO模型,基于早期的SPARCv8 TSO模型。当时他们声称"据我们所知,x86-TSO是可靠的,足够强大以供编程使用,并且与供应商的意图基本一致。" 几个月后Intel和AMD发布了新手册,广泛采用了这个模型。
似乎所有Intel处理器从一开始就实现了x86-TSO,尽管Intel花了一十年才决定承诺这一点。回顾过去,显然Intel和AMD架构师正在努力决定如何编写一个内存模型,既为未来处理器优化留出空间,仍然为编译器编写者和汇编语言程序员做出有用的保证。"尽可能强但不再更强"是一个困难的平衡行为。
ARM/POWER宽松内存模型
现在让我们看看一个更宽松的内存模型,在ARM和POWER处理器上找到的模型。在实现层面,这两个系统在许多方面都不同,但保证的内存一致性模型结果是相似的,比x86-TSO甚至x86-TLO+CC都弱得多。
ARM和POWER系统的概念模型是每个处理器读取和写入自己完整的内存副本,并且每次写入都独立地传播到其他处理器,在传播过程中允许重新排序。

这里没有总存储顺序。没有描述的是,每个处理器也被允许推迟读取直到需要结果为止:读取可以延迟到稍后的写入之后。在这个宽松模型中,我们迄今为止看到的每个litmus测试的答案都是"是的,这确实可能发生。"
对于原始的消息传递litmus测试,单个处理器对写入的重新排序意味着线程1的写入可能不会被其他线程以相同的顺序观察到:
Litmus测试:消息传递
这个程序能否看到 r1 = 1
, r2 = 0
?
// 线程 1 // 线程 2
x = 1 r1 = y
y = 1 r2 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:否。在ARM/POWER上:是的!
在ARM/POWER模型中,我们可以认为线程1和线程2各自拥有独立的内存副本,写入在这两个内存之间以任何顺序传播。如果线程1的内存在对线程2发送 x
更新之前发送 y
更新,并且如果线程2在这两次更新之间执行,它将会看到结果 r1 = 1
, r2 = 0
。
这个结果说明ARM/POWER内存模型比TSO更弱:它对硬件的要求更少。ARM/POWER模型仍然允许TSO所做的那种重新排序:
Litmus测试:存储缓冲
这个程序能否看到 r1 = 0
, r2 = 0
?
// 线程 1 // 线程 2
x = 1 y = 1
r1 = y r2 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:是的! 在ARM/POWER上:是的!
在ARM/POWER上,对 x
和 y
的写入可能已经写入本地内存但尚未在读取在相反线程上发生时传播。
这是之前说明x86具有总存储顺序意义的litmus测试:
Litmus测试:独立写入的独立读取(IRIW)
这个程序能否看到 r1 = 1
, r2 = 0
, r3 = 1
, r4 = 0
?(线程3和4能否以不同顺序看到 x
和 y
的变化?)
// 线程 1 // 线程 2 // 线程 3 // 线程 4
x = 1 y = 1 r1 = x r3 = y
r2 = y r4 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:否。在ARM/POWER上:是的!
在ARM/POWER上,不同线程可能以不同顺序了解不同的写入。它们不被保证就写入到达主内存的总顺序达成一致,所以线程3可以看到 x
在 y
之前变化而线程4看到 y
在 x
之前变化。
作为另一个例子,ARM/POWER系统具有可见的内存读取(加载)缓冲或重新排序,如这个litmus测试所示:
Litmus测试:加载缓冲
这个程序能否看到 r1 = 1
, r2 = 1
?(每个线程的读取能否发生在另一个线程的写入之后?)
// 线程 1 // 线程 2
r1 = x r2 = y
y = 1 x = 1
在顺序一致的硬件上:否。在x86(或其他TSO)上:否。在ARM/POWER上:是的!
任何顺序一致的交错执行必须从线程1的 r1 = x
或线程2的 r2 = y
开始。该读取必须看到零,使得结果 r1 = 1
, r2 = 1
不可能。然而,在ARM/POWER内存模型中,处理器被允许将读取延迟到指令流中稍后的写入之后,这样 y = 1
和 x = 1
在两个读取之前执行。
尽管ARM和POWER内存模型都允许这个结果,Maranget等人报告(在2012年)仅在ARM系统上能够经验地复现它,在POWER上从未成功。在这里,模型与现实之间的差异开始起作用,就像我们检查Intel x86时一样:实现比技术上保证的更强模型的硬件鼓励依赖于更强行为的程序,意味着未来的、更弱的硬件将破坏程序,无论是否有效。
就像在TSO系统上一样,ARM和POWER有屏障,我们可以在上述例子中插入以强制顺序一致的行为。但明显的问题是ARM/POWER没有屏障时是否排除任何行为。任何litmus测试的答案都可能是"不,那不可能发生"吗?当我们专注于单个内存位置时,它可以是。
这是一个即使在ARM和POWER上也不会发生的litmus测试:
Litmus测试:一致性
这个程序能否看到 r1 = 1
, r2 = 2
, r3 = 2
, r4 = 1
?(线程3能否看到 x = 1
在 x = 2
之前而线程4看到相反的情况?)
// 线程 1 // 线程 2 // 线程 3 // 线程 4
x = 1 x = 2 r1 = x r3 = x
r2 = x r4 = x
在顺序一致的硬件上:否。在x86(或其他TSO)上:否。在ARM/POWER上:否。
这个litmus测试与前一个类似,但现在两个线程都写入单个变量 x
而不是两个不同的变量 x
和 y
。线程1和2向 x
写入冲突的值1和2,而线程3和线程4都两次读取 x
。如果线程3看到 x = 1
被 x = 2
覆盖,线程4能否看到相反的情况?
答案是否定的,即使在ARM/POWER上:系统中的线程必须就单个内存位置的写入总顺序达成一致。也就是说,线程必须同意哪些写入覆盖其他写入。这个属性被称为一致性。没有一致性属性,处理器要么对内存的最终结果持不同意见,要么报告一个内存位置从一个值翻转到另一个值然后再回到第一个值。编程这样的系统将是非常困难的。
我故意省略了ARM和POWER弱内存模型中的许多微妙之处。更多细节,请参见Peter Sewell关于该主题的任何论文。另外,ARMv8通过使其"多副本原子"加强了内存模型,但我在这里没有空间解释这到底意味着什么。
有两个重要观点需要带走。首先,这里有难以置信的微妙之处,这是十多年学术研究的主题,由非常执着、非常聪明的人进行。我自己并不声称理解其中的全部内容。这不是我们应该希望对普通程序员解释的东西,不是我们在调试普通程序时希望能够搞清楚的东西。其次,被允许的与观察到的之间的差距为未来不愉快的意外创造了条件。如果当前硬件没有表现出允许行为的全部范围——特别是当很难推理出到底允许什么时!——那么不可避免地程序将被编写为意外地依赖于硬件更受限的行为。如果新芯片在其行为中限制性更小,新行为破坏你的程序这一事实在技术上是硬件内存模型允许的——也就是说,错误技术上是你的错——这并不能带来多少安慰。这不是编写程序的方法。
弱排序和无数据竞争的顺序一致性
现在我希望你相信硬件细节是复杂而微妙的,不是你每次编写程序时都想搞清楚的。相反,如果有捷径形式的帮助会很好:"如果你遵循这些简单的规则,你的程序将只产生某种顺序一致交错的结果。"(我们仍然在谈论硬件,所以我们仍然在谈论单个汇编指令的交错。)
Sarita Adve和Mark Hill在他们的1990年论文"弱排序——一个新定义"中正是提出了这种方法。他们将"弱排序"定义如下:
让同步模型成为一组对内存访问的约束,指定何时以及如何进行同步。
硬件相对于同步模型是弱排序的当且仅当它对所有软件都表现为顺序一致这些软件遵守同步模型。
尽管他们的论文是关于捕捉当时的硬件设计(不是x86、ARM和POWER),但将讨论提升到具体设计之上的想法,使论文至今仍然相关。
我之前说过"有效的优化不会改变有效程序的行为。" 规则定义了什么意味着有效,然后任何硬件优化都必须保持这些程序像它们在顺序一致机器上那样工作。当然,有趣的细节是规则本身,即定义程序有效意味着什么的约束。
Adve和Hill提出了一个同步模型,他们称之为无数据竞争(DRF)。这个模型假设硬件有内存同步操作,与普通内存读取和写入分开。普通内存读取和写入可能会在同步操作之间重新排序,但它们不能跨越同步操作移动。(也就是说,同步操作也作为重新排序的屏障。)如果一个程序在所有理想化的顺序一致执行中,来自不同线程的对同一位置的任何两个普通内存访问要么都是读取,要么通过同步操作强制一个发生在另一个之前,那么这个程序就被称为无数据竞争的。

让我们看一些例子,取自Adve和Hill的论文(为了展示而重新绘制)。这里是一个执行变量 x
的写入后读取同一变量的单线程。这里没有竞争,因为一切都在单个线程中。
相比之下,这个两线程程序中存在竞争:

这里,线程2在没有与线程1协调的情况下写入x。线程2的写入与线程1的写入和读取都竞争。如果线程2正在读取x而不是写入它,程序将只有一次竞争,在线程1的写入和线程2的读取之间。每次竞争都涉及至少一次写入:两个不协调的读取不会相互竞争。
为了避免竞争,我们必须添加同步操作,这会在不同线程的操作之间强制排序,共享同步变量。如果同步S(a)(在变量a上同步,用虚线箭头标记)强制线程2的写入在线程1完成后发生,竞争就被消除了:现在线程2的写入不可能与线程1的操作同时发生。

如果线程2只是读取,我们只需要与线程1的写入同步。这两个读取仍然可以同时进行:

线程可以通过一系列同步来排序,即使使用中间线程。这个程序没有竞争:

另一方面,使用同步变量本身并不能消除竞争:有可能使用不当。这个程序确实有竞争:

线程2的读取与其他线程中的写入正确同步——它肯定在两个写入之后发生——但这两个写入本身没有同步。这个程序不是无数据竞争的。
Adve和Hill将弱排序呈现为"软件和硬件之间的契约",具体地说,如果软件避免数据竞争,那么硬件表现得就好像它是顺序一致的,这比我们在前几节中检查的模型更容易推理。
但是硬件如何才能履行其契约义务呢?Adve和Hill给出了一个证明,硬件"被DRF弱排序",意味着它将无数据竞争程序执行为好像通过顺序一致排序,前提是它满足一组特定的最低要求。我不会详细介绍,但重点是在Adve和Hill的论文之后,硬件设计师有了一个由证明支持的食谱:做这些事情,你就可以断言你的硬件将显得顺序一致对无数据竞争的程序。事实上,大多数宽松的硬件确实以这种方式工作,并继续这样做,假设同步操作的适当实现。Adve和Hill最初关注的是VAX,但当然x86、ARM和POWER也能满足这些约束。一个系统保证对无数据竞争的程序提供顺序一致性的外观通常被缩写为DRF-SC。
DRF-SC标志着硬件内存模型的一个转折点,为硬件设计者和软件作者提供了清晰的策略,至少对那些用汇编语言编写软件的人来说是这样。正如我们将在下一篇文章中看到的,高级编程语言的内存模型问题没有一个如此整洁的答案。
下一篇关于编程语言内存模型的文章将在这里继续。
致谢
这一系列文章极大地受益于与我(Russ Cox)在Google工作的许多工程师的讨论和反馈。我对他们表示感谢。我对任何错误或不受欢迎的观点承担全部责任。
有段时间,博主总是被 C++ 的几个内存序搞得迷惑,看 cppref 和一些工程代码更是莫名奇妙。直到看了 rsc 的这篇文章,才真正理解内存序的动机。
令博主震惊的是,这些内存序直到最十几年仍在发展,大幅度依赖硬件给于的能力和原语。
特此翻译备份留念。当读者对 C++ 的 memory order 仍然迷惑时,可以阅读此技术简史。Relax 内存的结构图,就是一个典型的分布式系统啊!
从此,博主接触某项技术时候,都会去有意识地搜索下相关的技术背景。甚至面试他人时,也会延伸问问面试者是否了解技术背景。真的是大有裨益。