try ai
科普
编辑
分享
反馈
  • 多线程

多线程

SciencePedia玻尔百科
核心要点
  • 多线程区分了并发(concurrency)和并行(parallelism):前者指在一段时间内管理多个任务,后者指在多个核心上同时执行任务。
  • 竞争条件(race condition)是多线程中的一个主要挑战,它源于对共享资源的非同步访问,可通过互斥锁和锁等机制来防止。
  • 如阿姆达尔定律(Amdahl's Law)所述,并行化带来的性能增益受到固有串行代码的限制,同时也受到伪共享等微妙的硬件层面问题的影响。
  • 有效的并行编程要求设计的算法与底层硬件相匹配,从数据结构中的细粒度锁到管理NUMA系统上的内存,皆是如此。

引言

在处理器速度增长停滞的时代,对更强计算能力的追求已转向内部,即转向同时做多件事情的艺术。这就是多线程(multithreading)的领域,它是现代计算机科学中的一个基础概念,有望带来巨大的性能提升,但也充满了微妙的复杂性。虽然看似简单,但编写正确且高效的并行程序的实践,需要对软件逻辑与硬件现实之间错综复杂的协作有深刻的理解。许多开发者都曾被那些无法简单分析的非确定性错误和性能瓶颈所困扰。

本文旨在引导读者穿越这一充满挑战的领域。我们将首先在​​原理与机制​​部分剖析核心概念,建立并发(concurrency)与并行(parallelism)之间的关键区别,探索线程如何维护私有状态,并审视用于防止竞争条件(race condition)混乱的同步工具。我们还将揭示隐藏的性能成本,从阿姆达尔定律(Amdahl's Law)的理论极限到令人困惑的硬件陷阱——伪共享(false sharing)。随后,在​​应用与跨学科联系​​部分,我们将看到这些原理的实际应用,追溯它们从单个处理器核心的设计到GPU上大规模科学模拟执行的影响,揭示驱动现代计算的普适模式。

原理与机制

要真正理解多线程的力量与风险,我们必须从一个类比开始,而非代码。想象一位大厨在厨房里工作。这位大厨就是我们的中央处理器(Central Processing Unit),即CPU。

同时做多件事的幻象与现实

我们的厨师接到了准备三道菜大餐的任务。锅里炖着汤,烤箱里烤着肉,还有沙拉用的蔬菜需要切。如果只有一个厨师,他们该如何应对?他们不会完全做完一道菜再开始下一道。相反,他们会切一些蔬菜,然后搅一下汤,再检查一下烤肉,接着回来继续切菜。在任何一个瞬间,这位厨师只在执行一个动作,但在几分钟的时间里,三道菜都在取得进展。他们在不同菜肴上的工作是交错进行的。

这就是​​并发(concurrency)​​的本质。它是一个系统在重叠的时间段内管理并推进多个任务的能力。一个在操作系统中运行数百个任务的单CPU核心就是一个并发系统。它执行一小部分任务,然后迅速切换到另一个任务,给人一种所有事情都在同时发生的错觉。

现在,想象我们又雇了两位厨师。厨房里现在有三位厨师。一位可以专门负责沙拉,另一位负责汤,第三位负责烤肉。在完全相同的时刻,一位厨师在切菜,另一位在搅拌,第三位在涂油。这就是​​并行(parallelism)​​:多个任务的同时执行。并行需要多个物理处理单元——在我们的例子中是更多的厨师,对计算机而言则是更多的CPU核心。

这种区别并非仅仅是语义上的,它对性能至关重要。我们可以设计一个实验来观察这种差异。假设我们有NNN个简单的、重复性的计算任务(我们的“线程”),以及一台拥有MMM个处理器核心的计算机,其中NNN远大于MMM。首先,我们强制所有NNN个线程在单个核心上运行。如果我们跟踪每个线程的进度,我们会看到一张图表,在任何瞬间,只有一个线程的进度条在攀升。这些进度条以交错、穿插的方式上升——这是一幅无并行的并发完美图景。接下来,我们释放所有MMM个核心。现在,我们的进度图将显示多达MMM条进度条在同时攀升。这就是并行中“同时执行”的可视化体现。能够区分这两种操作模式是掌握并发编程的第一步。

每个任务的私有工作区:调用栈

如果我们的并发厨师同时处理三个不同的食谱,他们必须小心不要混淆配料。汤里的盐绝不能跑到蛋糕糊里去。每道菜都需要自己独立的工作空间、自己的一套笔记和量好的配料。

同样,一个​​线程(thread)​​——程序代码中一条独立的执行路径——也需要自己的私有工作区。但如果两个线程同时执行同一个函数,会发生什么?它们如何保持各自的内 部变量相互独立?

答案在于一个优美而简单的数据结构:​​调用栈(call stack)​​。一个进程中的每个线程都拥有自己私有的调用栈。当一个函数被调用时,一个包含其局部变量、参数和返回地址的“栈帧”(stack frame)会被推入该线程的栈中。当函数返回时,该栈帧被弹出。

想象两个线程,T1T_1T1​和T2T_2T2​,都在执行同一个递归函数。递归就像一套俄罗斯套娃;每次调用都将一个新的套娃放入上一个里面。对计算机而言,每次递归调用都会在调用栈上放置一个新的栈帧。因为T1T_1T1​和T2T_2T2​有独立的栈,它们正在构建两个独立的栈帧塔。操作系统,我们的大总管,可能会在T1T_1T1​的塔建到一半时暂停它,让T2T_2T2​去建自己的塔。当它这样做时,它会小心地保存T1T_1T1​世界的状态——包括指向其栈顶的关键寄存器——然后再加载T2T_2T2​的上下文。当轮到T1T_1T1​再次运行时,它的状态被完美恢复,它继续建造它的塔,完全不知道自己曾被暂停过。两个栈永远不会混杂在一起。这种对私有工作区的优雅分离,使得线程能够共存而不会践踏彼此的局部数据。

共享的麻烦:竞争条件

虽然私有栈可以防止线程干扰彼此的局部变量,但多线程的真正挑战在于线程必须访问共享资源时。一个进程内的所有线程通常共享同一片主内存,包括全局变量。这就像我们的厨师们共享一个公共的食品储藏室。

这种共享导致了并发编程中最臭名昭著的错误之一:​​竞争条件(race condition)​​。当多个线程未经协调地访问一个共享资源,其中至少有一次访问是写入操作,并且最终结果取决于它们操作交错的不可预测顺序时,就会发生竞争条件。

考虑一个简单的场景:两个线程需要对一个共享对象执行一次性初始化。逻辑看似简单:检查标志位ready是否为false;如果是,则执行初始化并将ready设为true。这被称为“检查后行动”(check-then-act)模式。假设初始化操作是为一个共享计数器x(初始为000)加一。

可能会出什么问题?让我们追踪一种可能的执行路径:

  1. 线程T1T_1T1​读取ready。其值为false。
  2. 在T1T_1T1​能采取行动之前,操作系统抢占了它,并运行T2T_2T2​。
  3. 线程T2T_2T2​读取ready。其值也是false。
  4. T2T_2T2​进入“行动”阶段。它读取x(值为000),计算0+10+10+1,并将111写回x。然后它将ready设置为true。
  5. 现在T1T_1T1​再次获得运行机会。它很久以前就已经通过了“检查”阶段!它盲目地进入“行动”阶段。它读取x(值为111),计算1+11+11+1,并将222写回x。

初始化被执行了两次,最终状态是错误的。根据读写操作的确切交错顺序,x的最终值可能是111或222。结果是非确定性的——这是程序员的噩梦。

这个问题可能更加微妙。竞争条件可能导致​​撕裂读(torn read)​​,即一个线程读取一个正在被另一个线程更新的变量,从而得到一个新旧数据混杂的值。例如,如果一个线程将一个161616位的值分两次、每次888位写入,另一个线程可能在第一个888位块写入后、第二个写入前读取该变量,观察到一个混乱、无意义的值。这是因为标准的内存写入不保证是​​原子(atomic)​​的——即从所有其他线程的角度来看是不可分割和瞬时的。

强制秩序:同步与锁

为了防止竞争条件的混乱,我们需要强制建立秩序。我们必须确保当一个线程在操作共享资源时,没有其他线程可以干扰。访问共享资源的代码块被称为​​临界区(critical section)​​。为了保护它,我们使用​​锁(lock)​​。锁是一种同步原语,它强制执行​​互斥(mutual exclusion)​​,保证在任何给定时间最多只有一个线程能进入临界区。

可以把它想象成共享资源的“发言权杖”。只有持有权杖的线程才被允许发言(访问资源)。当它完成后,它会放下权杖,另一个等待的线程可以捡起它。

当一个线程试图获取一个已被持有的锁时,它应该做什么,主要有两种策略:

  • ​​互斥锁(mutex)​​(mutual exclusion的缩写)是一种阻塞式锁。如果一个线程发现锁被占用,操作系统会将其置于“睡眠”状态,并放入一个等待队列中。CPU随后可以自由地运行其他不相关的线程。当锁被释放时,操作系统会“唤醒”队列中的下一个线程。这就像一个在服务台前彬彬有礼的人;他们取一个号然后坐下来等待轮到自己。如果等待时间很长,这种方式非常高效,因为没有CPU时间被浪费。

  • ​​自旋锁(spinlock)​​是一种非阻塞式或*忙等待*锁。如果一个线程发现锁被占用,它会进入一个紧凑的循环,反复检查锁的状态直到它变为空闲。这就像一个不耐烦的人在猛敲一扇锁着的浴室门。它消耗CPU周期,并且在现代多核芯片上,当自旋的核心不断尝试读取锁的内存位置时,会产生一场缓存一致性流量的风暴。然而,如果已知等待时间极短(短于操作系统让线程睡眠再唤醒所需的时间),自旋锁实际上可能更快。

选择是一种权衡:在高争用情况下,互斥锁的阻塞策略对于整体系统吞吐量要优越得多。

更高级的技术甚至完全不使用锁,而是使用特殊的硬件原子指令,如​​加载链接/条件存储(Load-Linked/Store-Conditional, LL/SC)​​。这些指令允许一个线程说:“读取这个值,我将计算一个新值,然后我将尝试把它存回去,前提是在此期间没有其他人改变过原始值。”这是一种乐观的方法,但可能导致​​活锁(livelock)​​:所有线程都试图同时更新,它们的尝试都因为冲突而失败,于是它们都立即重试,再次失败,如此无限循环。它们都在忙于执行指令,但没有完成任何有用的工作。一个常见的解决方案出奇地简单:在重试前引入一个小的、随机的延迟。这使得尝试变得不同步,最终允许一个线程成功,就像一群人试图同时挤出门口,如果他们停止推挤,让一个人先走,会更快地通过一样。

并行计算的隐藏成本与微妙陷阱

即使有完美的同步,通往高性能的道路上仍然布满了微妙的陷阱。增加更多的处理器核心并不总能带来成比例的速度提升。

阿姆达尔定律与串行部分的束缚

一个程序的理论加速比受​​阿姆达尔定律(Amdahl's Law)​​的支配。简单来说,它指出并行化带来的性能增益受限于程序中固有串行的那部分。如果你任务的10%10\%10%必须串行完成,那么即使有无限数量的处理器,你也永远无法获得超过10x10\text{x}10x的加速比。

这个串行部分可能来自意想不到的地方。考虑一个95%95\%95%可并行的应用程序。在一台323232核的机器上,你可能期望获得巨大的加速。但如果每个线程都必须偶尔进行一个由操作系统内核内部的单个锁保护的系统调用,那么那个内核锁就成了一个新的、共享的瓶颈。所有323232个线程最终都会在一个队列中等待那一个锁。这种新的串行化开销,在单线程版本中是不存在的,会显著降低测得的加速比,将一个有前途的并行算法变成一个令人失望的现实世界表现者。

机器中的幽灵:伪共享

也许最违反直觉的性能陷阱是​​伪共享(false sharing)​​。现代CPU不是逐字节读取内存的;它们以称为​​缓存行(cache lines)​​(通常为646464字节)的块来获取内存。当一个核心向一个内存位置写入时,缓存一致性协议可能会使所有其他核心中该缓存行的整个内容失效,以确保它们不会使用过时的数据。

现在,想象两个线程在两个不同的核心上运行。线程1专门处理变量A,线程2专门处理变量B。从逻辑上讲,它们是独立的,不应该互相干扰。但如果由于命运的残酷捉弄,A和B恰好在内存中相邻,并落入同一个缓存行呢?

每当线程1写入A时,该缓存行对线程2就会失效。当线程2接着想写入B时,它必须重新获取整个缓存行,这反过来又使其对线程1失效。这两个线程,虽然逻辑上独立,却导致了缓存行的持续来回“乒乓”,极大地减慢了两者的速度。这被称为伪共享,因为它们实际上没有共享数据,但被迫争夺缓存行。解决方案通常是在数据结构中添加填充(padding),有意地将变量隔开,使它们落到不同的缓存行上。

事件的顺序:一个警示故事

最后,即使你的逻辑是完美的,你使用的工具也可能欺骗你。想象一下,你实现了一个正确的临界区,并添加了日志语句来观察事件的顺序。你运行代码,日志文件显示线程2在线程1进入临界区之前就退出了——这明显违反了互斥!在你抓狂地调试你的锁之前,请考虑一下日志记录机制本身。大多数I/O库使用每线程的缓冲区。你的线程不是直接将日志消息写入文件,而是写入内存中的一个临时缓冲区。这些缓冲区在不可预测的时间被刷新到磁盘。完全有可能线程1进入、记录到其缓冲区、退出,然后线程2做同样的事情,但它的缓冲区先被刷新到文件。文件中的顺序反映的是缓冲区刷新的顺序,而不是实际事件的顺序。你的锁建立的“先行发生”(happens-before)关系并没有被I/O系统遵守。保证日志顺序与执行顺序匹配的唯一方法是,将日志操作(包括刷新到磁盘)本身也作为临界区的一部分。

从软件到芯片

线程、并发和并行的概念不仅仅是抽象的软件模型。它们与现代处理器的物理设计紧密相连。一种称为​​同时多线程(Simultaneous Multithreading, SMT)​​(以其商业名称Hyper-Threading而闻名)的技术,允许单个物理CPU核心同时管理两个或多个硬件线程的状态。它有多套寄存器,但共享其主要的执行单元。对于操作系统来说,这个单核心看起来就像两个(或更多)逻辑处理器。

这优美地模糊了界限。在核心层面,通过在同一周期内从多个线程获取和发射指令,硬件表现得像一台​​MIMD​​(多指令多数据)机器。然而,运行在其上的单个线程可能只是简单的​​SISD​​(单指令单数据)程序。这种抽象的层叠——从你在代码中创建的软件线程,到操作系统调度它的逻辑处理器,再到执行它的物理核心的一部分——是硬件和软件之间错综复杂的协作的证明,正是这种协作使得现代计算成为可能。

应用与跨学科联系

在我们迄今的旅程中,我们已经探索了多线程的基本原理——即如何同时管理多个执行线程的逻辑。我们窥探了锁、信号量和条件变量的机制。但要真正领会这个思想的力量和精妙之处,我们必须看到它的实际应用。如同科学中的任何基本概念一样,它的美并非在孤立中显现,而是在其丰富的应用织锦中得到最深刻的揭示。现在,让我们开始一次巡礼,从硅处理器的核心,到我们这个时代最宏大的计算挑战,见证并发的艺术如何塑造我们的世界。

机器之心:单个处理器内的并行

我们常常想象计算机处理器像一个勤奋的职员处理一堆文件一样,逐一执行我们的命令。然而,这幅图景早已不是现实。现代处理器更像一个繁忙的作坊,有多个专门的工作站,都渴望着工作。挑战在于,单条指令流——我们那串行的“一堆文件”——往往无法让所有工作站都保持忙碌。一个线程可能会因为等待数据从内存中到达而暂停,使得作坊的算术单元处于空闲状态。

我们如何改进这一点?如果我们再雇一个拿着不同一堆文件的职员呢?当第一个职员卡住等待时,第二个可以把一个任务交给一个空闲的工作站。这就是​​同时多线程(Simultaneous Multithreading, SMT)​​的精髓,这项技术你可能更熟悉它的商业名称——Hyper-Threading。它是线程级并行(Thread-Level Parallelism, TLP)的直接应用,用以掩盖单个指令流中固有的延迟和空隙。

但这种魔力并非没有代价。管理两个职员而不是一个,需要一些开销——协调、调度以及追踪每人进度的资源。向单个核心添加越来越多的线程并不会带来无限的性能。每个额外的线程都会增加这种管理开销,消耗作坊有限容量的一部分。在某个点上,协调的混乱会超过拥有更多工作可做的好处。存在一个最佳点,一个使机器吞吐量最大化的最佳线程数。超过这个点再添加线程实际上会降低性能,因为机器花在管理线程上的时间比执行有用指令的时间还多。这揭示了一个深刻的权衡,即并行工作的供给与利用它的架构成本之间的平衡,这场戏剧在驱动我们数字生活的芯片内部每秒上演数十亿次。

算法的艺术:将并发融入软件的基础

从硬件向上层移动,我们发现软件的基石——我们的数据结构和算法——必须为并行的世界重新构想。考虑一个简单的二叉搜索树,一个组织有序数据的基本结构。在单线程世界里,添加一个新元素就像沿着树走下去找到正确的位置一样简单。但当两个线程试图同时添加元素时会发生什么?

它们可能都试图将一个新节点附加到同一个父节点上,但一个的操作会覆盖并抹去另一个的操作——一个“更新丢失”的竞争条件。一个幼稚的解决方案是对整棵树加一个巨大的锁。一次只能有一个线程进行插入。这样做是正确的,但这就像为了让一个外科医生做手术而关闭整个医院一样。它牺牲了所有的并行性。

一个远为优雅的解决方案是​​细粒度锁(fine-grained locking)​​。我们不在树上放一个大锁,而是在每个节点上放一个小锁。要插入一个新键,一个线程锁定根节点,决定是向左还是向右走,然后——这是关键步骤——它在释放当前节点上的锁之前,先锁定其路径上的下一个节点。这种技术,被称为​​锁耦合(lock-coupling)​​或​​手递手锁(hand-over-hand locking)​​,沿着树创建了一条安全链。它确保在线程遍历树的任何部分时,该部分不会被修改,同时又允许不同的线程在树的不同、不重叠的部分上同时工作。这种方法避免了死锁,因为锁总是以一致的、自顶向下的顺序获取。同样“只在需要时、只为需要的时间锁定所需之物”的原则可以扩展到更复杂的任务,比如遍历一个图以确定它是否为二分图,其中每个顶点都可以被赋予自己的锁,以协调多个并发线程的着色工作。

商业与信息引擎

并发的原则不仅仅是学术性的;它们是我们全球信息和金融系统的基石。考虑一个证券交易所的电子撮合引擎。成千上万的买卖订单并发到达。系统必须处理它们,但核心任务——将一个买单与一个卖单匹配并更新官方订单簿——是一个必须完美串行化的临界区,以维持一个公平有序的市场。

在这里,我们看到了并发与并行之间区别的一个生动例证。该系统是高度并发的,因为它正在管理数千个在途订单。但撮合引擎本身是一个瓶颈;它不是并行的。即使在一台拥有数十个核心的机器上,一次也只能完成一笔交易。正如阿姆达尔定律教导我们的,如果工作中有很大一部分是固有串行的,增加更多的处理器只会带来递减的回报。吞吐量受限于那一个临界区的速度。

世界各地的交易所是如何处理每秒数百万笔交易的?他们不使用单一的、庞大的撮合引擎。他们使用​​分区(partitioning)​​或​​分片(sharding)​​的策略。他们不为所有股票设一个订单簿,而是为不同的股票代码或股票组创建独立的撮合引擎。一个处理'AAPL'的引擎可以在一个核心上运行,而一个处理'GOOG'的引擎可以在另一个核心上运行,实现真正的并行。他们将一个大的、串行化的问题,转变成了许多小的、独立的、可并行化的问题。

同样地,序列化瓶颈现象也出现在无数其他系统中。想象一个数据库有64个工作线程都准备好执行计算,但它们都偶尔需要访问一个被一个长时间运行的管理任务锁定的共享表。在该锁持续期间,系统呈现出一幅徒劳的景象:64个线程都处于可运行状态,代表了高度的潜在并发性,但它们在数据库任务上却没有任何并行进展。系统很忙,但没有完成工作。这突显了并行潜力与已实现的并行之间的关键区别,后者最终由资源争用和同步决定。

发现的前沿:大规模并行科学

也许多线程最惊人的应用是在科学和工程模拟中,我们在那里构建虚拟宇宙,以理解从蛋白质折叠到星系形成的一切。这些问题通常非常庞大,需要远超我们已讨论过的并行级别。

这是​​图形处理器(GPU)​​的领域。GPU是并行架构的杰作,包含数千个简单的核心,旨在对不同的数据片执行相同的程序。这种执行模型被称为​​单指令多线程(Single Instruction, Multiple Threads, SIMT)​​。这是一个绝妙的抽象:程序员编写一个内核,就像为单个线程编写一样,而GPU则同时在数千个线程上启动它。硬件负责将这些线程分组为“线程束”(warps),以步调一致的方式执行指令,只要线程们做的事情相似,就能达到令人难以置信的效率。

许多自然法则的本质是“数据并行”的——相同的物理定律适用于空间中的每个点、系统中的每个粒子或网格中的每个元素。例如,在计算力学中,桥梁某一部分的应力和应变可以独立于其他部分进行计算,然后再组装成一个全局图像。这与SIMT模型完美契合。然而,当材料在应力下表现不同时,一个有趣的挑战就出现了——一些部分可能弹性拉伸,而另一些则塑性变形。处理塑性点的线程必须执行一个不同的、更复杂的“返回映射”算法。当同一线程束内的线程走了if-else语句的不同分支时,硬件必须串行化它们的路径,这种现象称为线程束分化(warp divergence)。这说明了问题物理特性与机器架构之间深刻而微妙的协作。

为了在这些机器上达到峰值性能,必须“像硬件一样思考”。考虑创建直方图的问题,这是数据分析中的一个基本工具。如果GPU上的数千个线程试图为一个共享的直方图中的箱子(bin)递增计数,它们会造成交通堵塞。首先,如果多个线程试图更新同一个箱子,它们必须原子地执行,而这些​​原子操作(atomic operations)​​将被串行化,造成争用瓶颈。其次,GPU的快速共享内存被组织成“bank”,就像高速公路上的车道。如果太多线程试图访问落入同一个bank的内存地址,它们会导致​​bank冲突(bank conflicts)​​,它们的访问也会被串行化。幼稚的方法会因这两种效应而慢如爬行。

解决方案是并行思维的典范。为了解决原子争用,我们使用​​私有化(privatization)​​:我们不为整个线程块设置一个大的直方图,而是给每个小组(一个线程束)在快速共享内存中一个自己的私有迷你直方图。现在,只有线程束内的线程可能相互争用。为了解决bank冲突,我们使用​​填充(padding)​​:我们在数据结构中添加未使用的虚拟字节来改变内存布局,确保常见的访问模式均匀地分布在内存bank上。

这些数据移动和同步的模式在科学计算中是普遍存在的。在像物质点法(Material Point Method, MPM)这样的方法中,用于模拟雪和沙等物质,我们看到一种“散布-收集”(scatter-gather)模式。在​​散布​​阶段,来自数百万个粒子上的数据被“抛”到一个背景网格上。这是一个典型的多对一操作,充满了写冲突。解决方案是我们已经见过的:使用快速的​​原子操作​​来管理碰撞,或者,更优雅地,使用​​图着色(graph coloring)​​来在同步阶段处理不冲突的粒子集。随后的​​收集​​阶段,粒子通过从网格中“拉取”数据来更新,这个阶段令人愉快地无冲突,因为许多线程可以从同一位置读取而没有问题。

最后,即使在超级计算机的规模上,问题被分割到许多节点上,内存局部性仍然是王道。在具有​​非统一内存访问(Non-Uniform Memory Access, NUMA)​​的现代多插槽服务器中,每个处理器都有访问速度快的“本地”内存和连接到另一个处理器、访问较慢的“远程”内存。一个并行流体动力学模拟,如果它在生成线程时没有考虑其数据所在的位置,将会被缓慢的远程内存访问所瓶颈。然而,一个NUMA感知的程序会确保数据被分配在将要操作它的处理器的本地内存上,例如通过使用“首次接触”(first-touch)分配策略。通过这样做,它可以利用所有处理器的全部内存带宽,与一个幼稚的、NUMA无感知的方法相比,其性能可以有效地翻倍。

一个算法的性能是由计算速度还是内存访问速度限制,这是一个核心问题。用于此分析的一个强大工具是​​屋顶线模型(Roofline model)​​。它通过算法的​​计算强度(arithmetic intensity)​​——即浮点运算次数与内存访问字节数的比率——来表征一个算法。通过将其与机器的峰值性能和内存带宽进行比较,可以立即诊断出代码是计算密集型还是内存密集型,从而指导所有进一步的优化工作。这个优美的概念为高性能计算的艺术带来了定量的清晰度,连接了机器学习和计算经济学等截然不同的领域。

从单个CPU核心中的SMT逻辑到超级计算机节点上NUMA感知的内存放置,从数据结构的细粒度锁到大规模科学模拟中的原子操作和着色方案,我们在每个尺度上都看到了相同的基本思想在回响。多线程不是一种单一的技术,而是一个由多种技术构成的宇宙,是我们的算法逻辑与赋予它们生命的机器物理现实之间丰富而演进的对话。它是协作的科学,也是驱动现代计算的引擎。