try ai
科普
编辑
分享
反馈
  • 硬件事务内存

硬件事务内存

SciencePedia玻尔百科
核心要点
  • 硬件事务内存(HTM)通过让程序员将代码封装在事务中,使其能够原子性地执行(如同一个“全有或全无”的单一操作),从而简化了并发编程。
  • HTM的工作原理是推测性地执行代码,并巧妙地复用硬件的缓存一致性协议来检测数据冲突,确保线程间的隔离性。
  • 事务可能因数据冲突、超出硬件容量限制、系统事件(如中断)或性能陷阱(如伪共享)而失败(中止)。
  • 一个使用HTM的稳健系统必须包含一个回退路径,例如传统锁,以保证在事务反复中止时程序仍能继续执行。

引言

在多核处理器的时代,管理对共享数据的并发访问是软件开发中最持久的挑战之一。依赖锁的传统方法虽然有效,但众所周知难以正确使用,常常导致死锁、可伸缩性差和代码复杂等问题。这在硬件的并行处理能力与程序员安全高效地利用这种能力之间造成了巨大的鸿沟。硬件事务内存(HTM)作为一种革命性的基于硬件的方法应运而生,旨在弥合这一鸿沟,它提供了一种优雅的抽象,有望简化并发编程。

本文将对硬件事务内存进行全面探讨。我们将首先剖析其核心原理和机制,揭示处理器如何利用推测执行和缓存一致性来营造出原子、隔离操作的假象。随后,我们将涉猎其多样化的应用和跨学科的联系,展示HTM如何加速遗留代码、在编译器中启用新的优化,并从根本上改变我们设计数据结构以及与操作系统交互的方式。读完本文,您不仅会理解HTM的工作原理,还将学会如何进行事务性思考,以构建更快、更可靠的并发软件。

原理与机制

在并发编程这个繁忙的世界里,多个执行线程同时运行,程序员们长期以来一直在与一个根本性挑战作斗争:如何在不引起混乱的情况下协调对共享数据的访问。传统的工具是​​锁​​。一个线程获取锁,在其“临界区”内执行更新,然后释放锁。这就像繁忙咖啡店里的卫生间钥匙——一次只能有一个人拥有它。虽然原则上简单,但锁是臭名昭著的错误来源,从死锁(两个线程永远等待对方的锁)到当许多线程被单个粗粒度锁阻塞时导致的性能不佳。

我们能否有更好的方法?我们能否仅仅在一段代码周围画一个边界,然后告诉计算机:“请让这一切一次性发生,就像在一个不可分割的步骤中一样。如果做不到,就假装它从未发生过。”这就是​​事务内存​​美好而宏伟的梦想。

原子块的梦想

从本质上讲,硬件事务内存(HTM)为程序员提供了一个极为简单的抽象。您无需手动管理锁,而是将您的临界区封装在一个事务中:

loading

这段代码块带有两个铁板钉钉的保证,通常被称为事务的“ACID”属性,尽管在HTM中我们主要关注两个:​​原子性(Atomicity)​​和​​隔离性(Isolation)​​。

​​原子性​​是“全有或全无”的承诺。当事务结束时,它的所有更改(新的余额、新的日志条目)要么同时对系统的其余部分可见,要么一个都不可见。不存在余额已更新但日志未更新的中间状态。事务要么作为一个单一的原子单元完成,要么消失得无影无踪。

​​隔离性​​保证了在您的事务运行时,它看起来像是宇宙中唯一发生的事情。它与所有其他并发运行线程的影响相隔离。从它的角度来看,世界的状态是静止的,除了它自己所做的更改。

这种编程模型是理智的庇护所。它承诺将程序员从获取和释放锁的复杂舞蹈中解放出来,让他们专注于程序的逻辑,并确信硬件将强制执行正确性。但处理器究竟是如何实现这种魔法的呢?

一座推测的水晶宫

处理器实际上无法停止世界来执行一个事务。相反,它进行了一种大胆的乐观行为:它进行​​推测​​。当遇到一条 begin_transaction 指令时,处理器基本上是在说:“我赌我可以在不干扰任何人,也没有任何人干扰我的情况下运行这段代码。”它开始执行指令,但其所有影响都暂时保持为临时状态,隐藏在系统的其他部分之外,仿佛置于一座隐喻性的水晶宫中。

为了管理这种假象,处理器秘密地跟踪事务接触的每一个内存位置。它维护两个列表:

  • ​​读集(read-set)​​:事务已读取的所有缓存行地址的记录。
  • ​​写集(write-set)​​:事务已写入的所有缓存行地址的记录。新数据被推测性地存储,通常在处理器的私有L1缓存中,但尚未被持久化或对其他核心可见。

如果事务顺利到达其 end_transaction 指令,它会尝试​​提交​​。在一个辉煌而瞬间的时刻,处理器将写集中的所有推测性写入变为永久性的,并使其全局可见。水晶宫变成了现实。

但如果在任何时候出了问题——如果这场赌博没有成功——事务必须​​中止​​。处理器只需丢弃所有推测性写入,清空其读集和写集,并将其状态恢复到事务开始前的样子。为此,硬件需要在事务开始时保存寄存器状态的“检查点”,并知道如何使推测性内存写入无效。 水晶宫破碎了,不留下一丝存在的痕迹。然后可以重新开始执行,要么重试该事务,要么回退到不同的策略。

看不见的守护者:缓存一致性

这种推测、提交和中止的模型很优雅,但它取决于一个关键问题:处理器如何知道何时出了问题?它如何检测到隔离性的破坏?

答案是计算机体系结构中最优美的协同效应之一。HTM系统没有发明全新的机制,而是巧妙地复用了使多核处理器成为可能的硬件:​​缓存一致性协议​​。

想象一个现代多核芯片。每个核心都有自己的私有缓存,这是一个小而快速的内存,用于存放最近使用过的数据副本。一致性协议,如常见的​​MESI(修改、独占、共享、无效)​​协议,是一套确保所有核心对内存有一致视图的规则。这是核心之间持续进行的高速协商。例如,如果核心A想要写入核心B拥有副本的数据,核心A的缓存必须向核心B发送“无效”消息,告诉它其副本现在已过时。

HTM利用这种现有的通信作为事务的无形守护者。其工作方式如下:

  1. 当核心A上的一个事务读取一个内存位置(一个缓存行 ℓ\ellℓ)时,它将 ℓ\ellℓ 添加到其读集,并以 Shared 状态将该行保存在其缓存中。
  2. 如果另一个核心B稍后尝试写入同一个缓存行 ℓ\ellℓ,其缓存将广播一个无效请求。
  3. 当核心A的缓存控制器看到这个针对其活动事务读集中某行的无效请求时,它便知道核心A读取的值已不再有效。隔离性保证已被破坏!硬件会立即触发​​冲突中止​​。

同样,如果核心A的事务写入一个缓存行,它必须首先获得该行的独占所有权。如果核心B随后尝试读取或写入同一行,一致性协议将检测到冲突,核心A将中止其事务。为保持缓存同步而构建的硬件,成为了事务隔离性的执行者,以零软件开销检测冲突。

当水晶宫破碎时:事务为何中止

虽然这种推测机制很强大,但对成功、隔离执行的赌注可能会因多种原因而失败。理解这些失败模式是有效使用HTM的关键。

  • ​​数据冲突​​:这是最常见的原因。如上所述,如果两个事务试图以不兼容的方式访问同一个缓存行(例如,读-写或写-写),其中一个或两个都将中止。冲突的概率自然会随着线程数(NNN)的增加或事务持续时间(ttt)的变长而增加,从而产生一个更大的易受攻击窗口。

  • ​​容量中止​​:硬件跟踪读集和写集的能力是有限的。这种跟踪通常在处理器的L1缓存或类似结构中完成。如果一个事务变得太大——触及的独特缓存行数量超过了硬件所能跟踪的范围——它将触发容量中止。例如,一个试图修改超过典型32 KiB L1缓存中可用的512个缓存行的事务,注定会失败。

  • ​​系统事件​​:事务是用户级的推测,但它运行在由操作系统(OS)管理的处理器上。如果OS需要介入——例如,处理页错误、服务外部中断或执行抢占式上下文切换——它通常无法在事务的推测性上下文中这样做。唯一安全且简单的操作是让硬件中止事务,处理系统事件,然后让程序决定如何继续。 这在OS希望抢占式管理资源的愿望与事务希望运行至完成的愿望之间造成了根本性的紧张关系。

  • ​​伪共享​​:这是一个特别隐蔽的中止原因。想象两个线程,T1T_1T1​ 和 T2T_2T2​,需要更新完全独立的变量,比如 counter_A 和 counter_B。如果碰巧这两个变量在内存中相邻,它们可能位于同一个64字节的缓存行上。当 T1T_1T1​ 写入 counter_A 且 T2T_2T2​ 写入 counter_B 时,它们没有共享任何数据。但我们的守护者——缓存一致性协议,只在缓存行的粒度上工作。它看到对同一行的两次写入,并尽职地发出冲突信号,导致中止。这就是​​伪共享​​。解决方案通常是在我们的数据结构中添加填充,以确保由不同线程修改的数据位于不同的缓存行上,这是以内存空间换取并发性的直接权衡。

与不完美共存:事务性世界的策略

HTM并非万能灵药,其性能取决于处理不可避免中止的智能策略。

首先,一个稳健的系统需要一个​​回退路径​​。如果一个事务反复中止——可能是由于高数据争用,或者因为它从根本上来说对于硬件的容量来说太大了——系统不应该永远重试。在尝试一定次数后,它应该放弃乐观的事务性方法,回退到使用传统的锁。这提供了一个至关重要的安全网,确保总能取得进展,即使速度不如成功的事务那样快。

其次,复杂的系统需要​​争用管理​​策略。考虑一个场景,一个长时间运行的事务持有一个许多其他短而快的事务需要的“热”缓存行。这个长事务就像一个恶霸,导致较短的事务反复中止。这被称为​​事务性优先级反转​​。为了解决这个问题,先进的HTM系统可以实现公平机制。例如,当首次在某行上检测到冲突时,缓存目录硬件可以启动一个计时器。如果阻塞事务在特定时间(TTT)内没有释放该行,硬件可以向长时间运行的事务发送强制中止信号,让等待的事务继续进行。这种“冲突租约”方法确保没有单个事务可以无限期地饿死其他事务。

归根结底,硬件事务内存代表了计算机科学中一个深邃的思想。它利用了缓存一致性、推测执行和流水线控制等复杂的底层机制,并从中锻造出一个清晰、强大且直观的编程模型。它没有消除并发的挑战,而是重新定义了它们,用一种乐观、概率性的推测和恢复世界取代了确定性但难以推理的锁。它证明了在复杂但统一的硬件原理之上构建简单抽象之美。

应用与跨学科联系

现在我们已经探索了硬件事务内存(HTM)的内部工作原理,我们可以踏上一段更激动人心的旅程:看它在实践中的应用。一个物理原理的真正美妙之处不仅在于其定义,更在于它以出人意料且优雅的方式解决了不同领域的问题。HTM不仅仅是一项巧妙的芯片工程技术;它是一个新的视角,通过它我们可以审视并发这一棘手挑战,将曾经是复杂逻辑噩梦的问题转变为更易于管理,有时甚至是优美的事物。

最简单的魔术:让旧锁更快

HTM最直接、最直观的应用是为并发工具箱中最古老的工具之一——锁——注入新的活力。程序员使用锁来保护共享数据,就像一个房间的“一次一人”规则。但这条规则可能效率低下。如果你有一百个只想看房间里有什么的人(读者)和只有一个想改变某物的人(写者),让读者们互相等待似乎是一种浪费。这导致了复杂的“读写”锁。

HTM提供了一个非常优雅的替代方案。想象一下,读者们决定采取乐观态度。他们直接走进房间而不去获取读锁,但在这样做的时候,他们悄悄地“订阅”了主门的锁。他们只需在自己的事务内读取锁变量即可实现这一点。现在他们可以自由浏览了。如果一个写者过来并锁上了主门,这个动作——对锁变量的写入——会立即为所有乐观的读者触发警报。他们的事务会被硬件自动中止,仿佛他们从未存在过。然后他们可以回退到“B计划”:耐心等待写者完成,然后正常获取读锁。这个回退至关重要,因为HTM是乐观的;它不保证成功。为了确保程序总能取得进展,而不是陷入无休止的重试循环,必须在切换到有保证但较慢的方法之前,设定有限的尝试次数。

这种“事务性锁省略”是一个强大的模式。我们甚至可以在快速的事务路径和慢速的锁路径之间建立更复杂的“握手”。例如,我们可以使用一个版本号 vvv 来代替简单的锁,每当写者进入和离开其临界区时,该版本号就递增。事务性读者在进入时检查版本号,并在提交前再次检查。如果数字变了,它就知道有写者出现过,于是它会中止。但真正的魔力在于,对 vvv 的初始读取也布设了硬件的绊索。如果在事务活动期间,有写者试图增加 vvv ,事务会因冲突而立即中止。这是一个双层防御系统,结合了硬件绊索和软件健全性检查。

原子性作为一种超能力

取代锁仅仅是个开始。HTM真正的力量在于其​​原子性​​的承诺——能够让一系列复杂的操作看起来像一个单一的、不可分割的步骤。

考虑一个复杂的数据结构,如B树,它几乎被用于所有的数据库和文件系统中。插入一个新元素可能很简单,但如果一个节点已满,它必须被分裂,并且它的中值键必须被“提升”到其父节点。这可能导致父节点分裂,如此等等,形成一连串的更改在树中向上蔓延。用细粒度的锁来协调这一切是出了名的困难且容易出错。有了HTM,想法简单得惊人:将整个插入逻辑,包括所有潜在的分裂和提升,都包装在一个大的事务中。如果事务提交,树就从一个有效状态原子性地转换到另一个有效状态。如果它因任何原因中止,树将保持原样,我们可以回退到使用一个单一、简单、缓慢的全局锁来完成工作。程序员可以提供两种实现:一种是简单且正确的(基于锁的回退),另一种是快速且乐观的(HTM版本)。

这种超能力超越了主内存。想象一下,你正在编写一个程序来控制图形处理单元(GPU)。你可能会花时间仔细构建一个大的命令缓冲区——一个供GPU渲染复杂3D场景的指令列表。你必须确保GPU要么看到旧的命令缓冲区,要么看到新的、完全构建好的命令缓冲区,但绝不能是写了一半、已损坏的混乱状态。HTM对此非常完美。CPU可以在一个事务内准备整个缓冲区。由于事务的写入是隔离的,GPU在提交那一刻之前什么也看不到,直到整个缓冲区在内存中完全形成。只有到那时,CPU才会向一个特殊的“go”寄存器写入(一个MMIO write)来启动GPU,从而确保GPU始终使用一致的数据。

魔法的规则:当咒语失效时

任何足够先进的技术都与魔法无异,但作为科学家,我们知道总有规则。理解事务为何失败,能让我们对机器本身有更深刻的洞察。

最常见的失败原因之一是​​冲突​​。但什么构成冲突?硬件看不到你的变量;它看到的是内存系统的物理现实:缓存行。缓存行是CPU移动内存的最小单位,通常是64字节。如果两个线程修改两个不同的8字节计数器,而它们恰好位于同一个64字节的缓存行上,硬件会报告冲突,其中一个事务将中止。这种现象,被称为​​伪共享​​,是并行编程中的一个经典陷阱。HTM使其后果变得异常清晰。解决方案不是更复杂的逻辑,而是更周到的数据布局:通过添加填充来确保每个计数器都位于其私有的缓存行上,伪冲突就消失了。这种通过给线程更多“活动空间”来减少争用的原则是通用的。一个所有线程都争夺同一些头尾指针的幼稚生产者-消费者队列,将会遭受高事务中止率。一个更聪明的设计将队列划分为多个块,从而大大降低了冲突的概率。

事务也可能因为变得太大而失败。硬件用于推测性更改的“记忆”是有限的,通常与L1缓存的大小有关。如果一个事务修改的缓存行超出了缓存所能容纳的数量,就会发生​​容量中止​​。例如,一个更新大型有限状态机的操作,可能会触及内存中如此多的不同部分,以至于它根本无法容纳在一个事务中。试图将更新“分块”成两个较小的事务是一个致命的错误,因为它破坏了我们所追求的原子性;如果第一个事务提交而第二个中止,系统将处于一个被破坏的、不一致的状态。

也许最根本的限制是,HTM只能回滚其自身的推测性内存写入。它无法撤销在外部世界有副作用的动作。它不能“取消发送”一个网络包或“取消敲响”一个铃铛。试图在事务内部执行I/O或系统调用会因此导致它中止。解决方案不是违抗物理定律,而是要巧妙。事务可以不直接执行I/O,而是执行一个纯内存操作:将一个I/O请求排入一个共享缓冲区。然后一个独立的后台工作线程可以安全地从这些请求中取出并与外部世界交互。这个优美的模式,被称为​​延迟I/O​​,通过将原子决策与不可回滚的动作分离开来,优雅地绕过了这个限制。

将HTM编织进计算的结构中

HTM的原理是如此基础,以至于它们出现在看似不相关的计算机科学领域中,并在它们之间建立了联系。

在​​编译器设计​​中,HTM促成了大胆的新优化。考虑一个操作顺序很重要的循环(它们是不可交换的)。编译器不能天真地并行运行循环的迭代,因为那会改变最终结果。然而,它可以生成这样的代码:每个迭代在事务内部并行运行,但有一个转折:它使用一个“票号计数器”来确保事务只能按正确的顺序提交。迭代5可以与迭代1并行开始执行,但在迭代4提交其结果之前,它在物理上被阻止提交自己的结果。这允许了推测性的并行执行,同时严格保留了原始程序的语义。这是对HTM的精湛运用,不仅是为了原子性,更是为了有序原子性。

在​​操作系统​​和​​内存管理​​中,HTM与其他高级并发机制以微妙的方式相互作用。例如,一些无锁数据结构依赖​​危险指针​​来防止一个节点的内存在另一个线程试图访问它时被释放。这要求一个线程通过写入一个可见的“危险”位置来公开其使用一个节点的意图。在这里我们看到了一个深刻的矛盾:HTM的隔离性要求其推测性写入不可见,而危险指针则要求它们可见。简单地将危险指针的发布放在事务内部是不安全的,因为回收线程将看不到它。正确的解决方案需要理解两个系统:必须在开始事务之前发布危险指针,确保节点受到保护,然后再进行原子操作。这突显了一个深刻的真理:你不能简单地将并发机制拼接在一起。你必须根据它们的基本假设来组合它们。一个同样优美且可行的解决方案是使用​​基于纪元的回收​​,其中事务订阅一个全局纪元计数器。内存回收器在释放内存之前增加此计数器,这会自动触发所有活动事务的中止,切断它们对即将被释放内存的访问。

从取代锁到赋能编译器和启发操作系统设计,硬件事务内存远不止是一个硬件特性。它是一种思维工具。它简化了某些问题,但在此过程中,它迫使我们直面机器的物理现实——缓存行的现实、缓冲区的有限性以及I/O不可逆转的时间之箭。通过给我们一个强大但有限的“撤销”按钮,它鼓励我们寻找更优雅、更有洞察力的方式来构建我们的程序,揭示了软件和硬件之间永恒舞蹈中固有的美和统一。

begin_transaction() // Read and modify shared data balance = balance - 100; log.add("withdrawal"); end_transaction()