
并行处理是现代计算的引擎,其力量无處不在,幾乎支撐著我們使用的每一項技術。然而,要真正駕馭其力量,我们必须超越“使用更多核心”這一簡單想法,深入探究其背后的支配原则。一个常见的困惑点——并发与并行之间微妙而关键的区别——往往是第一个绊脚石。本文旨在填补这一空白,通过清晰、基础性的阐述,解释并行系统从物理实现到理论极限的工作原理。在接下来的章节中,我们将首先剖析核心概念,然后见证它们的实际应用。您将学习定义并行及其挑战的基本原则和机制,然后探索其在科学、工程等领域的变革性应用和跨学科联系。
在我们理解并行处理的旅程中,我们必须首先厘清两个经常被混淆但本质上截然不同的概念:并发(concurrency)与并行(parallelism)。正确理解这一区别是解开其他一切的关键。
想象一位技艺高超的厨师独自在厨房工作。这位厨师正在准备一顿包含多道菜的复杂餐点。他为沙拉切蔬菜,然后转身搅拌 simmering sauce,检查烤箱里的烤肉,接着又回去切菜。从观察者的角度来看,沙拉、酱汁和烤肉都在同一时间准备——它们的准备时间是重叠的。这就是并发。它是在同一时间段内管理并推进多个任务的艺术。然而,在任何一个瞬间,厨师只在做一件事:切菜、搅拌或检查。这些任务是*交错(interleaved)*执行的。
现在,想象我们雇佣了第二位厨師。一位廚師可以準備沙拉,而另一位則同時製作醬汁。这就是并行。多个任务在完全相同的时刻被执行,因为我们有多个工人。并行是并发的一种形式,但它需要多个物理执行单元。
在计算机中,单个中央处理器(CPU)核心就像那位单身厨师。通过一种名为时间分片(time-slicing)的巧妙技巧,操作系统可以每秒在不同程序或线程之间切换数千次。这给人一种错觉,即许多事情同时发生。但实际上,只是一个核心在以惊人的速度处理多个任务。如果我們對這樣的系統進行思想實驗,就能清楚地看到這一點。如果我们在一个单核上运行 个计算密集型线程,每个线程大约只能获得处理器 的注意力。当你增加更多线程时,每个独立线程的进度会成比例地减慢,而系统完成的总工作量并不会增加——核心固定的处理能力只是被稀释了。
这种并发的错觉非常有用,尤其对于涉及等待的任务。考虑一个从网络获取数据的现代应用程序。异步系统允许 CPU 核心(我们的厨师)不必为空等缓慢的网络(烤箱预热)而闲置,而是可以切换去做其他事情,比如响应用户的点击。当数据到达时,系统会重新拾起该任务。这就是响应式用户界面和高效 Web 服务器背后的魔力:它不是用户代码的并行执行,而是单线程上巧妙的非阻塞并发。
为了实现真正的并行,我们需要更多的厨师。在计算领域,这意味着更多的硬件:多个 CPU 核心。有了多个核心,我们就可以从单个厨师的厨房升级到 полноценной сборочной линии。
考虑一个构建为三阶段流水线的数据处理任务:*生产者(producer)*创建数据,*过滤器(filter)*处理数据,*消费者(consumer)*完成数据。在单核上,这只是一系列步骤。为了处理一个项目,核心必须执行所有三个步骤,总时间是它们持续时间的总和:。
但有了三个核心,我们可以为每个阶段分配一个核心。当第一个项目从生产者移动到过滤器时,生产者可以立即开始处理第二个项目。当第一个项目移动到消费者时,第二个项目移动到过滤器,第三个项目进入生产者。所有三个核心都在并行地处理不同的项目。这就是流水线并行(pipelined parallelism)。
这立即揭示了并行系统的一个深刻原则:瓶颈。我们整个流水线的吞吐量受限于其最慢的阶段。如果过滤阶段需要 ,而其他阶段耗时更短,那么整个流水线每 只能产出一个成品。提高其他阶段的速度也无济于事,除非我们加快瓶颈阶段的速度。
自然界以其优雅的方式,甚至在一个核心和两个核心之间創造了一个中间地带。这项技术被称为同步多线程(Simultaneous Multithreading, SMT),或称超线程(Hyper-Threading)。这就像一个核心拥有一些重复的内部资源,使其能够在同一个时钟周期内处理来自两个不同线程的指令。这就像一个厨师有两双手,但仍然只有一个大脑。SMT 通过填补原本浪费的执行槽位,可以提供真实但有限的性能提升。这是一种硬件并行形式,但与拥有第二个完整的核心不同,因为线程仍然争夺核心的许多关键资源。为了真正看到区别,人们可以设计一个实验:将多个线程固定到一个核心上观察纯粹的并发(交错进展),然后再将它们释放到多个核心上观察真正的并行(同步进展)。
那么,如果4个核心很好,4000个核心会好一千倍吗?严峻的答案是,几乎总是否定的。这是由于一个名为阿姆达尔定律(Amdahl's Law)的基本原则。
大多数程序并非完全可并行化。它们包含本质上是串行的代码部分——一个一次只能由一个线程执行的临界区,或者一个必须首先完成的初始设置。阿姆达尔定律指出,你可以实现的最大加速比受限于程序的串行部分。
想象一个任务,其中 30% 的工作是可并行的,70% 是严格串行的。你可以投入一百万个核心来处理这 30% 的部分,将其时间缩减到几乎为零。但 70% 的串行部分仍然需要同样长的时间。它成为了最终的瓶颈。该程序的理论最大加速比为 。无论你增加多少硬件,都无法使这个程序快过 43%。这揭示了一个深刻的真理:算法本身的性质对并行性能施加了硬性限制。
看待這個問題的另一種方式是通過工作-深度模型(Work-Depth model)。想象一个并行计算是一个依赖图。操作的总数是工作量(Work, )。通过这个图的最长依赖计算路径是深度(Depth, ),也称为跨度(span)或关键路径(critical path)。这条路径代表了一系列操作链,其中每一步都依赖于前一步。这些操作必须按顺序执行。因此,无论你有多少处理器——即便是无限个——总执行时间永远不会小于深度 。如果一个算法的结构使其深度随输入规模线性增长(),那么从根本上就不可能使其在亚线性时间内运行。处理器的数量无法克服长依赖链。
为什么程序会有串行部分?因为并行任务很少是独立的。它们需要通信和协调。这种协调行为正是棘手之处,也常常是阿姆达尔定律所描述的串行瓶颈的来源。
一个简单但有些笨重的协调机制是屏障(barrier)。想象一个程序,其工作被划分为多个阶段。屏障强制所有线程在一个阶段结束时等待,直到每个线程都到达。只有那时,它们才能一起进入下一个阶段。每个阶段的时间由该阶段中最慢的线程决定。这可以防止较快的线程超前,但能确保正确性。然而,这也使阶段之间的转换串行化,限制了并行性。
一个更细粒度的问题是保护一段共享数据,比如一个需要多个线程递增的计数器。在老式的单处理器上,你可以通过短暂禁用中断来解决这个问题——实际上是告诉世界“别烦我”,在你执行关键更新时。但在多核系统上,这是无用的;另一个核心不受你本地中断掩码的影响,可能会同时访问数据。
这就是硬件必须提供解决方案的地方。现代 CPU 提供原子指令(atomic instructions)。这些是硬件保证其不可分割的特殊操作(如 compare-and-swap 或 fetch-and-add)。它们从内存中读取一个值,修改它,然后写回,作为一个单一的、不可中断的步骤,即使在所有核心之间也是如此。这些原子操作是并行系统上锁(locks)和互斥锁(mutexes)等所有更高级同步原语的基本构建块。
这些锁定机制的设计可能会产生巨大的后果。一个著名的现实世界例子是在某些编程语言解释器中发现的全局解释器锁(Global Interpreter Lock, GIL),如 CPython。GIL 是一个保护整个解释器状态的单一互斥锁。这意味着即使你有一台强大的 16 核机器和一个有 16 个线程的程序,在任何给定时刻,这些线程中只有一个能够真正执行 Python 字节码。其他 15 个线程,即使被操作系统调度到其他核心上,也会卡住等待锁。对于 CPU 密集型任务,GIL 实际上将并行机器变成了一个并发的单核机器,完全抵消了多核的优势。解决方法是什么?使用多进程而非多线程,因为每个进程都有自己的解释器和 GIL。
但锁本身充滿危險。最令人畏惧的之一是死锁(deadlock)。想象一个 CPU 上的线程获取了一个锁。然后,同一个 CPU 上发生了一个紧急的硬件中断。中断处理程序代码运行,并试图获取同一个锁。处理程序现在会空转,等待锁被释放。但是锁被它刚刚中断的线程持有。那个线程在处理程序完成之前无法运行以释放锁。而处理程序永远不会完成,因为它在等待线程。这是一种致命的拥抱,一个数字版的“第二十二条军规”,它將凍結整個系統。编写正确的并行代码需要在这样的潜在灾难雷区中航行。
我们已经到达并行处理中最深奥、最反直觉的方面。当两个核心同时执行时,它们访问“共享内存”意味着什么?令人欣慰的幻觉是一个单一、巨大的内存库,其中每次写入都立即对所有人可见。现实则要奇怪得多。
每个 CPU 核心都有自己的私有缓存,以及关键的存储缓冲区(store buffer)。当一个核心执行写操作时,数据通常首先被放入这个私有缓冲区。然后核心可以继续执行其他指令,而无需等待将数据发送到主内存系统的缓慢过程。这意味着存在一个微小但关键的时间窗口,在此期间,一个核心的内存“视图”與其他核心不同。这被称为弱内存模型(weak memory model)。
這導致了奇異而詭異的結果。考虑一个经典的试金石测试。我们有两个共享变量, 和 ,初始值都为 。
x = 1; 然后读取 的值。y = 1; 然后读取 的值。两个线程都读到 是可能的吗?逻辑上似乎说不。其中一个写入必须“先”发生,所以另一个线程应该能看到它。但在并行世界中,答案是肯定的!核心 1 对 x=1 的写入进入其存储缓冲区。核心 2 对 y=1 的写入进入它的存储缓冲区。在这些写入通过缓存一致性协议变为全局可见之前,核心 1 可以读取 的旧值(即 ),而核心 2 可以读取 的旧值(即 )。这种现象,即数据竞争(data race)的可观察效应,是在具有弱内存模型的硬件上并行执行的直接后果。有趣的是,这个结果在单核上幾乎是不可能的,因为两个线程是交错执行的,它们会通过该核心的存储缓冲区和缓存看到一致的内存视图。真正的并行不仅让事情变快,它改变了规则。
我们如何恢复理智?我们使用内存屏障(memory fences)(或内存栅栏)。内存屏障是一条特殊指令,它告诉 CPU 核心暂停并整理其事务。例如,它可能强制核心刷新其存储缓冲区,并等待所有这些写入被系统其他部分确认后,才允许执行任何更多的内存操作。通过在我们的试金石测试中的写和读之间插入一个屏障,我们强制事件的顺序,并使“两个都为零”的结果成为不可能 [@problemid:3627066]。
从厨房里两位厨师的简单想法开始,我们的旅程引领我们穿越了装配线、普适定律和同步的复杂舞蹈,一直深入到硬件内存模型中幽灵般的幻象。并行处理不仅仅是增加更多核心的问题;它是一种根本不同的计算方式,有其自身优美的原则、深刻的局限和深邃、微妙的挑战。
现在我们已经探讨了并行处理的基本原理,让我们踏上一段旅程,看看这些思想在实践中的应用。对速度的追求将我们引向何方?你可能会感到惊讶。并发和并行的原则不仅仅是计算机科学家的抽象概念;它们是现代技术賴以建立的基石,从你口袋里的智能手机到预测气候的超级计算机。更重要的是,它们为我们观察世界本身提供了一个强大的新视角。
让我们从一个熟悉的设备内部开始我们的旅程:一台现代计算机。当我们说一台计算机有一个“多核处理器”时,我们谈论的是线程级并行。这就像厨房里有几位独立的厨师,每位都能同时处理不同的菜谱。但还有另一种更微妙的并行形式在起作用。现代处理器还配备了特殊指令,通常称为 SIMD(单指令,多数据),它允许一个厨师用一把非常宽的刀一次切好八根胡萝卜。这是数据级并行。
这两种并行类型与操作系统的调度程序(扮演厨房经理的角色)之间发生着迷人的舞蹈。经理可能会指派两位厨师( 和 )在两个独立的台面(两个核心)上制作两道不同的复杂菜肴,这是线程级并行。同时,每位厨师可能正在使用他们的宽刀一次处理多种食材(数据级并行)。然而,厨房经理只看到两位厨师在做两道菜;每位厨师使用特殊工具加快工作速度是他们自己执行的细节。如果有三位厨师但只有两个台面,经理会让他们轮流工作,从而产生并发——所有三个人都在取得进展的错觉。通过这种方式,现代计算机是一个由操作系统并发管理的多层次并行交响曲。
这种管理至关重要。考虑经典的“生产者-消费者”问题。想象一个进程(生产者)正在生成数据,另一个进程(消费者)正在处理它。如果它们在两个不同的核心上运行,它们可以并行工作。但如果生产者更快怎么办?它会很快超前并不得不等待。如果消费者更快怎么办?它将大部分时间用于等待数据。优雅的解决方案是一个简单的缓冲区,一个共享的数据等待区。一个小缓冲区足以解耦这两个进程,允许较快的进程处理一批项目,而较慢的进程则迎头趕上。这平滑了工作流程。在一个任务切换有开销成本的真实系统中,一个更大的缓冲区可以通过减少进程必须暂停和重新启动的频率来显著提高性能,让并行执行的美妙之处得以展现,而不被后勤成本所拖累。
当我们加入更多专用处理器,如图形处理单元(GPU)时,情况变得更加复杂。GPU 最初为渲染视频游戏而设计,是数据并行的大师,包含数千个微小、简单的核心。在现代科学计算中,我们经常看到主 CPU 和 GPU 之间美妙的协作并发。CPU,即“主脑”,准备一个大型计算任务(一个“核函数”),并异步地将其发送给 GPU。CPU 不会等待;它立即轉向下一个任务,也许是准备另一个核函数。与此同时,GPU,即“主力”,执行核函数,对数百万个数据点并行执行相同的计算。CPU 工作和 GPU 工作的这种重叠是设备间的并发,而 GPU 自身的执行则是在单个设备内并行处理的大规模展示。
理解这些原则使我们能够设计出更快的系统。想象一下构建一个高性能的 Web 服务器。一个请求可能会经过几个阶段:解析请求(CPU工作)、从数据库或其他服务器获取数据(I/O等待)、处理数据(CPU工作)以及将日志写入磁盘(I/O等待)。一个幼稚的单线程服务器会一个接一个地做这些事,卡在等待缓慢的网络或磁盘上。
一个好得多的设计是并行流水线。我们可以为每个阶段分配线程池。CPU 密集型阶段,如解析和处理,受益于并行——我们可以简单地在多个核心上运行它们以增加吞吐量。I/O 密集型阶段受益于并发。我们可以使用非阻塞技术,即一个线程发起一个数据库获取请求,然后不等待,立即转而处理另一个请求。这种将计算与等待重叠的能力是构建响应迅速和高吞吐量系统的本质。系统的整体速度最终将受其最慢阶段——瓶颈——的限制。通过这种方式分析系统,我们可以智能地分配资源以实现最佳性能。
這種思維方式導致了並行編程中基本模式的發現。最常見的之一是“归约”(reduction)。假設你是一位計算經濟學家,模擬一個擁有數百萬家庭的國家,並且你想計算總消費量,。你如何并行地做到这一点?你不能让你的百万个处理器核心都试图将其值添加到一个共享的总和上——它们会互相践踏,造成巨大的瓶颈。
优雅的解决方案是树形归约。想象一下,在一个二叉树的底部有 个值。在第一个并行步骤中,我们使用 个处理器来成对相加。在下一步中,我们使用 个处理器来加上第一步的结果。我们重复這個过程,大约 步后,我们得到了最终的总和。总的加法次数与串行求和相同,大约为 ,但所需时间与树的高度 成正比。这是一个指数级的加速!这个简单的想法依赖于加法是结合律和交换律的。有趣的是,这只对完美的数学数字成立。对于计算机实际使用的浮点数,由于舍入误差,加法的顺序可能会轻微改变最终结果。因此,对于要求完美可重复性的科学应用,必须强制执行固定的归约顺序,以牺牲一点性能为代价来保证结果的确定性。
有时候,我们很幸运。有些问题是“易并行”的,意味着它们可以被分解成许多完全独立的任务。想象一下,你正试图量化气候模型中的不确定性。你可能需要用略微不同的初始参数运行整个庞大的模拟 10000 次。这些运行中的每一次都是一个完全独立的任务。使用“主-从”模式(master-worker),一个主进程可以简单地将这些任务分发给一个由 个工作处理器组成的集群。求解的总时间大约是单次模拟时间乘以 。这种加速几乎是完美的,随着处理器数量线性扩展,直到你的处理器数量超过任务数量。这是并行计算的圣杯,在科学和工程领域是一个惊人常见且强大的范式。
当我们进入拥有数十万甚至数百万核心的超级计算世界时,新的挑战出现了。你如何让所有这些处理器都忙于有用的工作?一个幼稚的方法是使用一个所有处理器都去检查的单一全局“待办事项”列表,这会因竞争而彻底失败。
一种名为“工作窃取”(work-stealing)的杰出的、去中心化的策略应运而生。每个处理器维护自己的私有任务列表。它将新工作添加到列表底部,并从底部取走工作。这使其最近使用的数据在其本地缓存中保持“热”状态,从而保持了局部性。但当一个处理器工作用完时会发生什么?它變成一個“小偷”,從另一個隨機選擇的處理器的列表頂部竊取一个任务。从顶部窃取可以获得最旧的任务,这很可能是一大块工作,从而有效地平衡了整个系统的负载。这个优美的算法结合了两全其美:大多数时候具有良好的数据局部性,以及在需要时具有强大的负载平衡能力。这是现代并行编程语言能够在大规模核心上高效执行复杂、动态任务的关键原因之一。
让我们看一个具体的例子:使用相场模型模拟二元合金中晶体结构的生长。这些由 Cahn-Hilliard 方程等方程控制的模拟,通常在巨大的三维点網格上进行。解决此类方程的一个强大技术涉及快速傅里叶变换(FFT)。要在分布式内存超级计算机上并行化此操作,不能简单地给每个处理器一部分问题。FFT 需要全对全通信(all-to-all communication)。标准方法是“铅笔分解”(pencil decomposition),即将三维网格沿两个维度划分,给每个处理器一条长长的“铅笔”状数据。为了执行三维 FFT,机器必须执行大规模的数据转置,这实质上是处理器之间数据的全局洗牌。
在這裡,我们遇到了并行计算的一个基本限制:通信。当我们扩展到更大的机器(弱扩展)或试图用更多的处理器解决一个固定的问题(强扩展)时,每个处理器的计算时间可能会减少,但等待数据跨越机器的时间——即延迟——会开始占据主导地位。理解计算和通信之间的这种权衡是高性能计算的核心所在。对并行的追求甚至重塑了算法本身。经典的数值方法,如求解微分方程的龙格-库塔(Runge-Kutta)格式,是为串行机器设计的。为了使它们适应并行硬件,数值分析学家巧妙地重新设计了底层公式,创造了可以并发执行的计算块,将串行依赖链转变为一系列并行冲刺。
最后,让我们退后一步,看看这些想法是多么普遍。并行计算的语言——处理器、工作、依赖关系、关键路径——不仅适用于计算机。它是一个理解任何复杂的、分布式过程的框架。
考虑一个分布式拒绝服务(DDoS)攻击。攻击者调集一个由数千台受感染计算机组成的“僵尸网络(botnet)”。在这种情况下,“处理器”是什么,“工作”又是什么?被控制的僵尸计算机就是处理器。“工作”就是它们生成的恶意请求总数。攻击的有效性是并行的函数——即同一瞬间发送请求的僵尸计算机数量。通过这种方式建模攻击,我们可以像分析任何其他并行算法一样分析其结构和复杂性。
这种思维方式无处不在。一个国民经济可以被看作是由数百万代理人(家庭、公司)并发决策的并行系统。摩天大楼的建造是一个具有复杂依赖图的并行项目。一个活着的有机体是一个由細胞進行通信和執行專門功能的巨量並行系統。
因此,并行处理的研究不仅仅是让我们的计算机更快。它为我们提供了一种新的直觉,一种描述和分析我们所生活的复杂、互联、并发世界的新语言。它证明了自然法则与计算法则之间美妙的统一性。支配硅芯片中信息流动的相同原理,也回响在市场的运作和生命的演化之中。通过理解这些原则,我们不仅能制造出更好的工具,还能更深刻地体会现实本身那错综复杂的并行之舞。