try ai
科普
编辑
分享
反馈
  • 剖析内存模型:从硬件到高级语言

剖析内存模型:从硬件到高级语言

SciencePedia玻尔百科
核心要点
  • 现代处理器采用宽松内存模型及存储缓冲区等优化措施,这可能重排内存操作,打破直观的顺序执行模型。
  • 内存一致性模型为跨处理器的内存操作可见性提供全系统规则,这一概念不同于缓存一致性,后者管理对单个内存地址的更新。
  • 程序员使用内存屏障和释放-获取语义等工具来强制指定顺序,创建“先行发生”(happens-before)关系,以保证并发程序中数据共享的正确性和安全性。
  • 语言内存模型(如 C++11 中的模型)在程序员、编译器和硬件之间建立了至关重要的契约,将高级排序请求转换为正确的低级指令。

引言

从单核到多核处理器的转变是计算史上最重要的变革之一。这一转变在释放巨大性能潜力的同时,也打破了程序员曾习以为常的简单线性世界。当多个线程并发执行、共享同一内存时,我们对顺序和时间的直观理解便会失效。如果一个处理器核心写入一个值,其他核心何时能看到它?又是以何种顺序看到?若没有一套明确的规则,并行世界中的编程将陷入一片混乱。

本文旨在通过揭示​​内存模型​​的奥秘,弥合单线程直觉与多线程现实之间的根本认知鸿沟。内存模型是一本至关重要的规则手册,它定义了来自不同线程的内存操作如何交互,从而为并发执行恢复了秩序和可预测性。通过理解这些规则,我们就能编写出正确、高效且可靠的软件,从而驾驭现代硬件的真正力量。

在接下来的章节中,我们将开启一段从抽象到实践的旅程。在“原理与机制”一章中,我们将探讨各种基本概念,从理想化的顺序一致性到当今 CPU 使用的宽松模型,并探索用于驾驭它们的工具,如屏障和原子操作。随后,在“应用与跨学科关联”一章中,我们将看到这些原理如何成为从操作系统设备驱动程序和无锁数据结构,到大规模科学模拟乃至区块链技术等一切事物的基石。

原理与机制

在我们日常的单线程思维世界里,事件以一种令人舒适的线性序列展开。一个念头接着一个念头,我们世界的状态以一种可预测、有序的方式演进。早期的程序员很自然地将这种直觉带入了他们的编程工作中。对于单个处理器核心而言,这是一种完全合理的思维模型。尽管现代编译器和处理器为了性能会在底层对指令进行激进的重排、流水线化和并行化处理,但它们恪守着一个神圣的约定:最终结果将永远如同代码是完全按照书写顺序逐行执行的一样。这就是顺序执行的宏大幻象。

但是,当我们进入多个处理器共享同一内存的领域时,会发生什么呢?两个事件“同时”发生意味着什么?如果一个处理器核心向内存写入一个值,其他核心何时能看到它?那条令人舒适的单一时间线碎裂成了多个视角。为了恢复秩序,我们需要一套新的规则——​​内存一致性模型​​。

单一时间线的梦想

我们能想到的最直观的规则被称为​​顺序一致性(Sequential Consistency, SC)​​。想象一下,所有处理器核心的指令流就像几副独立的扑克牌。如果一次执行对应于将这几副牌交错洗成一叠牌的某种全局顺序,并且满足一个关键约束:来自任何一副原始牌堆的牌的相对顺序必须保持不变,那么这次执行就是顺序一致的。每个处理器观察这同一叠牌,都会认同相同的全局事件历史。

这听起来异常简单,也正是我们天真地期望发生的情况。然而,即使是这个“完美”的世界也可能产生令人惊讶的结果。考虑两个线程 T1T_1T1​ 和 T2T_2T2​,共享变量 xxx 和 yyy 初始值均为 000:

  • T1T_1T1​:读取 yyy,然后向 xxx 写入 111。
  • T2T_2T2​:读取 xxx,然后向 yyy 写入 111。

有没有可能两个线程都读到值 000?这似乎有违直觉。然而,SC 允许这种情况发生。我们只需找到一种有效的指令牌“洗牌”方式。考虑以下这种顺序:

  1. T1T_1T1​ 执行其对 yyy 的读取,得到 000。
  2. T2T_2T2​ 执行其对 xxx 的读取,得到 000。
  3. T1T_1T1​ 执行其对 xxx 的写入。
  4. T2T_2T2​ 执行其对 yyy 的写入。

这个序列尊重了两个线程各自的内部程序顺序,并产生了两个读取操作都看到初始值零的结果。这里的“怪异”之处并非源于规则的破坏,而是源于并行系统中简单且不可避免的延迟。即使在行为最规范的模型中,也不存在一个普适的“当下”。

性能契约:缓存一致性与内存一致性

如果连理想模型都有意外之处,那么现实则要离奇得多。现代处理器为了性能与魔鬼做了交易:它们默认不提供顺序一致性。原因很简单:速度。强迫每个内存操作在继续执行前都必须得到整个系统的确认,就好比一个委员会,每个成员都必须批准写下的每一个字。那样的话,进度将陷于停滞。

为了加速,每个处理器核心都有一个私有的​​存储缓冲区(store buffer)​​,就像一个个人发件箱。当一个核心想要写入一个值时,它草草记下这个变更,将其放入缓冲区,然后立即着手下一个任务,并相信“邮政服务”(即内存系统)最终会将这次写入送达其他所有核心。

这一架构特性导致了一种更弱但远为常见的模型,即​​完全存储定序(Total Store Order, TSO)​​,这也是我们熟悉的 x86 处理器所使用的模型。让我们通过另一个经典的思维实验来看看存储缓冲区能做什么。同样,xxx 和 yyy 初始值均为 000:

  • 线程 0:向 xxx 写入 111,然后读取 yyy。
  • 线程 1:向 yyy 写入 111,然后读取 xxx。

在 SC 模型下,两个线程不可能都读到 000。但在有存储缓冲区的情况下,这不仅是可能的,而且是 TSO 的一个标志性行为。过程如下:

  1. 核心 0 执行 x←1x \leftarrow 1x←1。该写入操作进入其本地存储缓冲区。主内存中 xxx 的值仍然是 000。
  2. 核心 1 执行 y←1y \leftarrow 1y←1。该写入操作进入其本地存储缓冲区。主内存中 yyy 的值仍然是 000。
  3. 核心 0 执行对 yyy 的读取。由于来自核心 1 的写入仍在它的缓冲区中,核心 0 的读取操作会访问主内存系统,并发现 y=0y=0y=0。
  4. 核心 1 执行对 xxx 的读取。它同样在内存中找到了旧值 x=0x=0x=0,因为核心 0 的写入仍在缓冲区中。

现在正是澄清该领域两个最易混淆的术语的绝佳时机:​​缓存一致性(cache coherence)​​和​​内存一致性(memory consistency)​​。

  • ​​缓存一致性​​是一个局部的、针对单个地址的属性。想象每个内存位置(如 xxx 或 yyy)都是一个巨大图书馆中的一本书。像 ​​MESI​​ 或 ​​MOESI​​ 这样的缓存一致性协议确保在任何时候都只有一个“主副本”在被编辑。所有观察者都会就对那本书的编辑顺序达成一致。它对其他书的情况只字不提。

  • ​​内存一致性​​是一个全局的、系统范围的属性。它是整个图书馆的规则手册。如果书 A 中的一张便条写着“我刚更新了书 B”,一致性模型是否保证你看到这张便条时,也一定会看到书 B 的更新?不一定!缓存一致性确保书 A 和书 B 的页面不会被撕裂,但内存一致性定义了你是否能保证按照它们发生的顺序看到它们的更新。

在我们的 TSO 示例中,缓存一致性没有被违反。地址 xxx 有一个有效的事件序列,地址 yyy 也有一个独立的有效事件序列。而内存一致性模型则允许了它们在整个系统中的可见性被重排。

蛮荒西部与屏障的兴起

对于 ARM 和 POWER 等架构的设计者来说,即便是 TSO 也过于严格了。他们创造了​​宽松(或弱)内存模型​​,进入了一个名副其实的内存排序“蛮荒西部”。在这些模型中,不仅存储操作的可见性可能被延迟,不同存储操作的可见性顺序也可能相互重排。邮政系统甚至不承诺按你寄信的顺序送信。

这会造成一个经典的风险。想象一个生产者线程准备好一些数据,然后设置一个标志来表示数据已就绪:

  • 生产者:x←datax \leftarrow \text{data}x←data;flag ←1\leftarrow 1←1。
  • 消费者:while(flag == 0)...;v←xv \leftarrow xv←x。

在一台宽松模型的机器上,这可能会彻底失败。处理器可以自由地让对 flag 的写入在对 xxx 的写入之前对消费者可见。消费者看到 flag=1,兴高采烈地去读取 xxx,结果却得到了……过时的、无用的数据。

为了驯服这种无序状态,架构师给了我们强制指定顺序的工具。最直接的工具是​​内存屏障(memory fence)​​(或称 barrier)。一个完整的屏障是一条指令,它实际上告诉处理器:“停下。在此屏障之后的所有内存操作,必须等到在此之前的所有内存操作都全局可见后才能发布。”在生产者的两个存储操作之间插入一个屏障可以解决这个问题。

一个更优雅的解决方案是​​释放-获取语义(release-acquire semantics)​​。它们不是大锤,而是手术刀。

  • 一个​​释放存储(release store)​​(例如 `flag←release1flag \leftarrow_{\text{release}} 1flag←release​1) 对其之前的操作起到屏障作用。它保证在其线程中所有发生于其前的内存写入,都会在该释放存储操作本身变得可见之前或同时变得可见。
  • 一个​​获取加载(acquire load)​​(例如 while (flag.load_acquire() == 0)) 对其之后的操作起到屏障作用。它保证在其线程中所有发生于其后的内存读写,只会在该获取加载操作完成后才执行。

当一个获取加载读取到一个释放存储写入的值时,一个​​“先行发生”(happens-before)​​关系就建立了。这是一种因果链。消费者知道,如果它看到了标志,那么它就保证能看到在标志被设置之前准备好的数据。这种优雅的配对在不引入完整屏障的沉重代价的情况下恢复了秩序。

三个世界:硬件、编译器和语言

到目前为止,我们讨论的都是处理器和硬件。但程序员不写机器码,他们用 C++、Java 或 Rust 这样的语言编程。这为我们的戏剧引入了第三个角色:​​编译器​​。编译器也是一个激进的优化器,如果它认为可以使程序更快,它会很乐意重排你的代码,而这远在硬件看到代码之前就发生了。

这意味着我们需要一个能约束程序员、编译器和硬件的契约。这就是​​语言内存模型​​的角色,例如 C++11 中引入的模型。它为程序员提供了原子类型和内存排序选项,这些选项会被翻译成适用于编译器和目标硬件的正确指令。

比方说,在 C++ 中,你写了一个 relaxed 原子操作。你这是在告诉编译器和 CPU:“我知道我在做什么。你们拥有最大的自由度,可以为了速度将此操作与其他独立的内存访问进行重排。” 但如果你使用一个 acquire 加载,你就在发布一个命令:这个加载操作作为一个单向门。代码中后续的任何内存访问都不能被重排到这个 acquire 加载完成之前发生。请注意,它对之前的操作没有任何约束;它不是一个双向屏障。

这个契约是神圣的。如果你使用一个 seq_cst 屏障,语言保证它会参与到所有 seq_cst 操作的单一全局顺序中。为了遵守这个承诺,编译器必须将该屏障视为一个硬性壁垒,不能将周围的宽松原子操作跨越它进行重排。然后,它必须生成正确的硬件指令(如 ARM 上的 DMB)来迫使 CPU 也这样做。这个抽象在所有三个世界中都成立。

知其边界:内存模型不做什么

只有一个概念的边界清晰了,我们才能真正理解它。内存模型很强大,但它们并不能解决并发编程中的所有问题。

首先,内存模型与​​原子性(atomicity)​​是不同的。一致性模型是关于原子操作排序的推理。但如果一个操作本身就不是原子的呢?想象一台 32 位机器试图读取一个 64 位的值。它可能需要分两次 32 位的块来读取。如果一个线程写入了值的高位部分,而另一个线程写入了低位部分,那么读取线程可能执行了第一次 32 位读取,然后被其中一个写入操作中断,再执行第二次 32 位读取。结果就是一个“撕裂读”(torn read)——一个由不同整体的部分组成的怪物般的值。这不是一致性失败,而是原子性失败。在 C++ 等语言中,试图对非原子变量执行此操作构成​​数据竞争(data race)​​,其结果是可怕的​​未定义行为(Undefined Behavior)​​:所有规则都不再适用。

其次,内存模型关乎​​安全性(safety)​​,而非​​活性(liveness)​​。安全性属性说“坏事永远不会发生”(例如,在使用正确同步的情况下,你不会读到过时的值)。活性属性说“好事最终会发生”(例如,一个想要运行的线程最终会得到运行的机会)。内存模型不保证活性。一个线程可能在尝试获取一个锁,像 SC 这样的内存模型能确保其读写操作被正确排序。但如果操作系统的调度器不公平,就是不给那个线程在锁空闲时运行的机会,该线程就会饿死。这是一个调度问题,而不是内存模型问题。

最后,内存模型管理的是低级内存访问。更高级别的抽象,比如操作系统的文件系统,有它们自己的规则。当你在一个 POSIX 系统中使用 O_APPEND 标志打开一个文件时,操作系统保证每次 write() 调用都是原子的——它会将你的数据放在文件末尾,而不会与其他写入交错。这是操作系统提供的强大的排序和原子性保证,并且无论 CPU 底层的内存模型如何,它都有效。你不需要在对文件的 write() 调用之间插入内存屏障;你只需要信任操作系统定义良好的抽象。

理解内存模型是一段旅程,从我们自己思想中简单有序的世界,到现代硬件混乱并行的现实。它揭示了支配我们数字宇宙的一个隐藏规则层,这是一场由硬件、编译器和软件共同参与的优美而复杂的舞蹈,它们协力维持着一个脆弱的秩序幻象。

应用与跨学科关联

在前面的讨论中,我们穿越了内存模型的抽象景观。我们熟悉了游戏规则——那些微妙而严格的、规定了一个处理器核心的动作如何以及何时对另一个核心可见的法则。我们谈到了重排、屏障、缓存一致性,以及 release 和 acquire 语义的精妙协作。你可能会不禁思考:“这一切都是为了什么?难道这仅仅是给计算机架构师的一个理论谜题吗?”

你会欣喜地发现,答案是一个响亮的“不”。这些规则并非抽象的约束,它们是我们整个数字世界赖以建立的基石。它们是无形的秩序之线,让现代硬件混乱的并行漩涡得以编织出连贯、可靠的软件。从你手机上的操作系统到模拟宇宙的超级计算机,从锻造你代码的编译器到保障数字资产安全的区块链,内存模型的原理无处不在。现在,让我们踏上一段新的旅程,去看看这些规则在何处焕发生机。

并发的基础:一次可靠的对话

究其核心,并发编程关乎通信。一个执行线程如何安全地将信息传递给另一个线程?想象一下,你想构建一个简单的数字邮箱。一个“生产者”线程写入一条消息,然后升起一个 flag 来表示邮件已到达。另一个“消费者”线程等待那个 flag,然后读取消息。还有什么比这更简单吗?

然而,在宽松内存模型的世界里,这个简单的行为充满了危险。处理器为了不懈地追求速度,可能会重排操作。消费者线程可能会在生产者实际完成消息写入之前就看到“邮件到了!”的 flag 升起。它打开邮箱,结果只发现一封写了一半的信,或者更糟,是昨天的垃圾邮件。为了防止这种情况,我们需要强制执行一条规则:在升起 flag 之前完成的所有工作,必须对任何看到该 flag 的人可见。

这正是释放-获取语义的工作。当生产者使用 release 存储来升起 flag 时,它在做出一个承诺:“在此之前我所做的一切,现在都已准备好让全世界看到。”当消费者使用 acquire 加载来检查 flag 时,它在要求系统遵守该承诺:“直到我确认 flag 已升起,我才会去查看消息数据。”这种配对确保了对消息数据的写入先行发生于对该数据的读取,从而防止消费者看到不完整的消息。这种基本的生产者-消费者模式是无数进程间通信(IPC)方案和无锁数据结构的基石。

这个思想远不止于简单的标志。考虑一个程序,它构建一个复杂的数据结构——比如说,一个包含许多字段的客户记录——然后需要将其交给另一个线程进行处理。生产者不能在消费者读取记录时就地更新它;那将是一片混乱。一个远为优雅的解决方案是,让生产者在私有空间中构建整个记录,只有当它完成后,才通过将其地址写入一个共享指针来“发布”它。[@problem-id:3625471] 在这里,内存模型再次成为我们的救星。通过使用 release 存储来发布指针,生产者保证了所有初始化记录字段的复杂写入操作与指针本身一同变得可见。消费者使用 acquire 加载来读取该指针,确保它看到的是一个形态完美、完整的对象,而不是一个半成品。

但是,这次对话中消费者的那一方呢?如果它试图走捷径会发生什么?想象一个消费者线程在一个紧凑的循环中,只是等待一个 flag 改变。这被称为自旋等待。为了高效,它可能在循环内部使用 relaxed 加载,这种加载不带任何排序保证。while (flag == 0) { /* spin */ }。一旦它看到 flag 变为 1,它就退出循环并立即读取相关数据。但这里有一个陷阱!一个聪明(但天真)的处理器或编译器可能会注意到,对数据的读取与对 flag 的检查是独立的,并且为了隐藏延迟,“推测性地”在循环结束之前就执行数据读取。结果呢?消费者读到了过时的数据,尽管它在片刻之后正确地观察到了 flag 的变化。这就是为什么一个 acquire 操作——无论是将 flag 的加载变成一个 acquire 加载,还是在循环后放置一个 acquire 屏障——都是不可妥协的。它竖起一道屏障,告诉处理器和编译器:“在我完成之前,不要执行任何后续的内存读取。”它强制规定了观察的顺序。

与物理世界的接口:驯服硬件

内存模型不仅调解 CPU 核心之间的对话;它还掌管着 CPU 与你电脑内部无数其他设备之间更为奇特的对话——显卡、网络适配器、存储控制器等等。这些设备通常以特殊内存地址的形式出现在 CPU 面前,这种技术被称为内存映射 I/O(Memory-Mapped I/O, MMIO)。向这些地址写入并不在于存储数据,而在于发送命令。

考虑一个需要重新配置硬件外设的操作系统设备驱动程序。驱动程序可能首先将一个新的配置值 vvv 写入配置寄存器 CCC,然后写入一个“门铃”寄存器 DDD 来告诉设备:“开始!应用新配置。”但是一个弱序处理器可能会重排这两个写操作。设备可能在新的配置对它可见之前就收到了“开始!”命令,导致它基于旧设置进行操作,从而引发不正确的行为或系统崩溃。

为了防止这种情况,驱动程序使用内存屏障。一个写内存屏障,通常称为 wmb(),放置在两个写操作之间,就像对 CPU 的一道命令:“确保对 CCC 的写入对设备可见后,再发布对 DDD 的写入。”类似地,在读取方面,如果 CPU 正在轮询设备状态寄存器 SSS 以查看工作是否就绪,就需要一个读内存屏障 rmb(),以确保对 SSS 的读取在任何后续对数据寄存器的读取之前完成。这可以防止 CPU 基于旧状态推测性地读取过时数据。这些屏障就像交通信号灯,为 CPU 和物理世界之间繁忙的十字路口带来了秩序。

当我们处理执行直接内存访问(DMA)且与 CPU 非缓存一致性的设备时,挑战会加剧。想象一个 CPU 在其主存中为网卡准备一个命令描述符。它写入描述符的所有字段,然后敲响网卡的 MMIO 门铃。网卡随后使用 DMA 直接从主存中读取描述符。这里我们面临两个问题。首先是我们已经见过的排序问题:门铃写入不能超越描述符写入。一个写屏障可以解决这个问题。但第二个问题是可见性问题。刚写入的描述符可能仍停留在 CPU 的私有缓存中,对系统的其余部分不可见。因为网卡不是缓存一致性的,它无法“窥探”CPU 的缓存;它只从主内存读取。如果数据不在那里,DMA 引擎将读到垃圾数据。

解决方案是一个两步过程。首先,驱动程序必须执行指令,显式地“清理”或“刷新”包含描述符的缓存行,将数据强制写回主内存。其次,它必须使用一个写屏障,以确保刷新操作和所有描述符的写入都在 MMIO 门铃写入发布之前完成。这种缓存维护和内存排序的组合,是与非缓存一致性设备进行安全通信的关键秘诀,构成了从处理器意图到设备行动的万无一失的指挥链。

程序现实的架构师:编译器

到目前为止,我们谈论的都是程序员在指导硬件。但在这个过程中有一个强大的中介:编译器。编译器的任务是将你的高级代码翻译成高效的机器指令,它会以你可能永远无法想象的方式对你的代码进行重排、转换和优化。因此,内存模型不仅仅是为程序员和硬件制定的一套规则,它也是编译器必须遵守的一份有约束力的契约。

如果你写了一个从 flag 进行的 load-acquire,后面跟着一个从 x 进行的加载,你就在表达一个意图:对 x 的读取必须在对 flag 的读取之后发生并受其排序。一个试图隐藏 flag 读取延迟的编译器,可能会试图将对 x 的加载调度到对 flag 的加载之前。内存模型禁止这样做。acquire 语义是在沙地上画下的一条红线。编译器不能将后续的内存操作跨越它移动到更早的时间点。这样做可能会破坏 happens-before 保证,并重新引入程序员试图防止的数据竞争,从而允许出现像看到 flag 被设为 1 但却读到与之相关的旧数据这样的结果——而对于一个正确同步的程序,C++11 内存模型等已明确定义这种情况是不可能发生的。

这揭示了程序员与编译器之间关系的更深层次真相,尤其是在 C++ 和 Java 等语言中。这些语言做出了一个强有力的承诺,称为“DRF-SC”保证:当且仅当你的程序是无数据竞争的(Data-Race-Free, DRF)——即所有对共享数据的冲突访问都由同步操作排序——语言承诺你的程序将表现得如同它在简单、直观的顺序一致性(SC)模型下运行一样。

这个承诺的另一面是,如果你的程序确实存在数据竞争,其行为在官方定义上就是“未定义的”。这不仅仅是一个警告;这是授予编译器假设你的程序行为良好且无竞争的许可证。这个假设解锁了大量强大的优化。例如,如果编译器看到一个循环重复读取字段 S.f,它可能会执行聚合体的标量替换(Scalar Replacement of Aggregates, SRA),在循环前将 S.f 加载到寄存器中一次,并在所有后续访问中使用该寄存器。在单线程世界中,这是完全安全的。在多线程世界中,只有当编译器能证明没有其他线程可以同时写入 S.f 时,这才是安全的——这是 DRF 契约授予的假设。如果你,作为程序员,通过在 S.f 上创建数据竞争而破坏了契约,SRA 优化将导致你的程序错过来自其他线程的更新,从而导致令人费解的错误行为。因此,内存模型是您与编译器之间这一关键契约的法律框架。

现代计算的统一原理:从科学到金融

基本原理的美妙之处在于其普适性。支配两个线程间简单标志的内存排序规则,同样可以扩展到组织最大规模的计算任务和最现代的数字系统。

考虑一下驱动现代科学的大规模模拟,比如在分子动力学模拟中模拟数百万个粒子的相互作用。为了在超级计算机上运行,问题通过“域分解”被分解,其中模拟空间的不同块被分配给不同的处理器。这些处理器可以是同一芯片上的核心,也可以是通过网络分隔的节点。这立即产生了两种截然不同的并行编程模型,它们都是其底层内存模型的直接反映。

在单个多核节点上,我们使用​​共享内存​​模型。所有线程共享一个地址空间,通过加载和存储进行隐式通信。硬件缓存一致性处理数据的可见性,而程序员则使用锁和屏障来建立 happens-before 排序,以便正确交换其域边界上的粒子数据。

在通过网络连接的节点之间,我们使用​​分布式内存​​模型。每个节点都是一个独立的进程(一个 MPI 秩),拥有私有地址空间。它们之间没有共享内存,没有硬件一致性。通信必须是显式的:一个节点将其边界数据打包成消息,并使用消息传递接口(Message Passing Interface, MPI)通过网络发送。happens-before 关系不是由硬件屏障建立的,而是由 MPI_Send 和 MPI_Recv 调用本身的语义建立的。当今最大的超级计算机所使用的混合模型是两者的完美结合:使用 MPI 进行节点间通信,使用共享内存线程进行节点内并行,两者分别由各自的内存和一致性规则所支配。

最后,让我们看一个当今最受关注的技术之一:区块链。在一个简化的区块链系统模型中,一个“验证者”核心可能会检查一笔交易的有效性,并将其放入一个共享内存池或 mempool 中。然后一个“矿工”核心轮询这个池,抓取一个已验证的交易,并将其包含在一个区块中。你可能已经猜到了,这就是我们的老朋友生产者-消费者问题,只是穿上了现代密码学的外衣。 验证者是生产者,写入交易数据(xxx),然后设置一个就绪标志(yyy)。矿工是消费者,检查 yyy 然后读取 xxx。没有正确的内存排序——无论是通过强制执行像顺序一致性这样的强模型,还是通过使用 release-acquire 对——由于宽松的内存重排,矿工可能会在看到就绪标志的同时,看到一个过时的、未验证的或不完整的交易。确保邮箱正确更新的那些架构原理,同样也帮助确保了进入分布式账本的交易的完整性。

从最低层的硬件接口到最高层的科学和金融计算,内存模型是秩序的无形源泉。它证明了简单、严格定义的规则能够从潜在的混乱中创造出一致性,从而造就了我们今天所居住的这个广阔、并行且强大的计算世界。