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

线程调度

SciencePedia玻尔百科
核心要点
  • 抢占式调度对于现代响应式系统至关重要,它可以防止任何单个线程独占 CPU。
  • 调度器必须了解硬件拓扑结构(如 NUMA 和 CPU 缓存),以优化多核系统的性能。
  • 优先级反转是一个关键问题,即低优先级线程阻塞高优先级线程,可通过优先级继承等机制解决。
  • 调度策略因目标而异,从通用计算中最大化吞吐量到实时系统中保证截止时间。

引言

在现代计算领域,我们很容易将单台机器上同时执行数十个任务的假象视为理所当然。在这种无缝体验的背后,是操作系统中一个复杂而关键的组件:线程调度器。调度器扮演着总指挥的角色,决定在任何特定时刻哪个任务可以使用 CPU。这并非易事,它涉及在相互冲突的目标之间进行微妙的平衡,包括最大化吞吐量、确保用户应用程序的响应性以及为所有进程提供公平的访问机会。调度器设计中的选择对系统的稳定性、性能和可预测性有着深远的影响。

本文将深入解析线程调度的艺术与科学。我们将首先探讨其基本 ​​原理与机制​​,从控制的核心理念到确保公平性的算法,再到现代多核处理器所带来的复杂挑战。随后,我们将审视这些原理的深远 ​​应用与跨学科联系​​,揭示调度如何塑造从云计算到实时系统的一切。

原理与机制

从本质上讲,操作系统是一位制造幻觉的大师。它利用一个中央处理器(CPU)——一个速度极快但本质上是顺序工作的单元,一次只能做一件事——来创造出十几个程序同时运行的生动假象。我们可以一边听音乐、编译代码,一边浏览网页。这个魔术是如何实现的?秘密就在于 ​​线程调度​​ 的艺术与科学。​​线程​​ 是可由调度器独立管理的最小编程指令序列。​​调度器​​ 则是内核的交通警察,决定在每一微秒哪个线程可以使用 CPU。

调度器的工作是在相互竞争的目标之间进行微妙的权衡。它希望最大化吞吐量(完成尽可能多的工作)、最小化响应时间(使交互式应用感觉敏捷)并确保公平性(给予每个线程应有的机会)。正如我们将看到的,这些目标常常直接冲突,而调度器的设计就是一场穿越各种迷人权衡的旅程。

主动让出还是被动剥夺:两种控制哲学

想象一条单行道。你如何管理交通?一种方法是“君子协定”:每位司机行驶一段合理距离后就靠边让其他人通过。这就是 ​​协作式调度​​ 的精髓。一个线程会一直运行,直到它自愿 ​​让出​​ (yield) CPU——也许是因为它在等待文件加载,或者仅仅因为它被编程为一个“好公民”。这种方式简单、优雅,且开销非常低。

但如果某个司机不是君子呢?如果一个线程陷入了漫长而复杂的计算中,从不靠边停车会怎样?整个系统就会陷入停顿。所有其他应用程序——你的音乐播放器、网页浏览器,甚至你的鼠标光标——都会冻结,等待这个自私的线程完成。这不仅仅是一个理论上的担忧。许多现实世界中的计算任务具有“重尾”分布特性,这意味着虽然大多数运行时间很短,但少数几次运行可能会异常漫长。协作式系统是脆弱的;其响应性完全取决于表现最差的那个线程。

在一个模拟复制服务器的思想实验中,我们可以定量地看到这种效应。如果一个后台任务有 10% 的几率长时间运行(比如半秒钟),那么服务器响应时间的变化性——或称“抖动”——将变得巨大。对于服务器和交互式系统至关重要的可预测性也就丧失了。

这就引出了第二种哲学:​​抢占式调度​​。在这里,操作系统不再是一个礼貌的观察者,而是一个独裁者。它设定一个计时器。当一个线程用完了其分配的时间——即它的 ​​时间量​​ (quantum) 或 ​​时间片​​ (time slice)——计时器就会触发中断。操作系统会强行停止该线程,保存其状态,并调度另一个线程。没有任何单个线程可以劫持 CPU。通过使用 ​​轮询​​ (round-robin) 算法循环遍历所有可运行的线程,操作系统保证没有线程需要等待超过一个可预测的、有界的时间才能轮到它。同一个实验表明,采用抢占式调度后,响应时间的方差急剧下降,减少了数百倍。这种控制的代价是每次上下文切换会带来更多开销,但在响应性和稳定性方面的收益是巨大的。因此,几乎所有现代通用操作系统都是抢占式的。

调度器中的调度器:用户线程与内核线程

那么,操作系统抢占式地调度“线程”。但从操作系统的角度来看,线程究竟是什么?这个问题引出了线程实现方式的一个根本性设计选择。

最直接的模型是 ​​系统竞争范围 (System-Contention Scope, SCS)​​,也称为 1:1 模型。你在应用程序中创建的每个线程都对应一个操作系统内核知道并直接管理的、真实可调度的实体。如果你程序中的一个线程阻塞以等待网络数据包,操作系统知道它被阻塞了,并且可以调度来自同一程序——或任何其他程序——的另一个线程来运行。这个模型简单、健壮且功能强大。

然而,这是有代价的。每次在线程之间切换时,你都必须进入内核,这是一个相对较慢的操作。如果你需要每秒切换数千次任务该怎么办?这催生了 ​​进程竞争范围 (Process-Contention Scope, PCS)​​ 的创建,也称为 M:N 或 M:1 模型。在这种模型中,一个用户空间库创建了许多快速、轻量的 ​​用户级线程​​,并将它们映射到较少数量的内核级线程上。在同一进程内切换用户线程可以完全在用户空间完成,无需昂贵的系统调用。一次切换可以像一个简单的函数调用一样快。

这听起来很棒,但它有一个主要的陷阱。内核是盲目的;它只看到并调度自己的内核级线程。如果你有一个进程,其 100 个用户线程运行在一个内核线程上,而该内核线程执行了一个阻塞式系统调用(例如,等待磁盘 I/O),那么所有这 100 个用户线程都会被冻结。为了解决这个问题,基于 PCS 的系统必须变得极其聪明。它们必须不惜一切代价避免阻塞式系统调用,转而依赖非阻塞 I/O 和事件通知机制,如 [epoll](/sciencepedia/feynman/keyword/epoll) 或 select。事实上,人们可以通过监视一个正在运行的进程来发现这一点:高频率的 [epoll](/sciencepedia/feynman/keyword/epoll) 调用和低频率的传统阻塞式读取,是一个复杂的 PCS 实现的强烈指纹。

我们可以精确地对性能差异进行建模。在 SCS 中,一个等待的线程被唤醒并运行所需的时间,仅仅是它在操作系统唯一的就绪队列中等待的时间。而在 PCS 中,这是一个两阶段的过程:首先,进程的内核线程必须由操作系统调度,然后 特定的用户线程必须由进程内部的用户级调度器调度。这可能导致更长、更复杂的延迟路径,突显了两个调度层面之间错综复杂的协作关系。

追求公平与按比例共享

给予每个线程相等的时间片看起来很公平,但如果某些任务比其他任务更重要呢?我们需要一种方法来给予线程 ​​按比例共享​​ CPU 的能力。两种优雅的算法应运而生来解决这个问题。

​​彩票调度​​ 是一种极其简单、基于概率的方法。你根据线程期望的份额给予它们“彩票”。在每个时间量,调度器都会举行一次抽奖。持有中奖彩票的线程得以运行。如果一个线程持有 50% 的彩票,那么平均而言,它将赢得 50% 的抽奖,并获得 50% 的 CPU 时间。它的适应性极好;如果一个新线程到来,它只需将其彩票加入彩池即可。

随机性的美妙之处也正是其主要缺点。虽然从长远来看是公平的,但在短期内可能非常不公平。一个线程被选中的次数遵循二项分布,其标准差随时间片数量的平方根 N\sqrt{N}N​ 而增长。对于需要平滑、可预测性能的应用来说,这种抖动可能是一个问题。

这就是 ​​步长调度​​ 发挥作用的地方,它是彩票调度的确定性“表亲”。想象一场赛跑,每个赛跑者的步长与他们的彩票数量成反比(彩票越多,步长越小)。每一步,我们都让覆盖总距离最短的赛跑者前进一步。这就是步长调度。每个线程都有一个 pass 值(已覆盖的距离)和一个 stride 值。调度器总是选择 pass 值最低的线程,让它运行,然后将其 pass 值增加其 stride 值。这种确定性的方法保证了 CPU 时间的分配与理想比例的偏差永远不会超过一个时间量。它实现了与彩票调度相同的按比例共享目标,但误差有界且最小。这种在随机简洁性与确定性精确性之间的根本权衡并非 CPU 调度所独有;它也出现在其他领域,如网络包调度,其中类似彩票的随机公平队列(Stochastic Fair Queuing, SFQ)就对应着类似步长的加权公平队列(Weighted Fair Queuing, WFQ)。

现代多处理器的迷宫

在单个 CPU 上的调度是一个已经解决的问题。但在现代多核、多处理器系统上的调度则是一个混乱而迷人的前沿领域。突然之间,问题不仅仅在于线程 何时 运行,还在于它 在何处 运行。

一个挑战是,并非所有并行应用程序都是“易于并行”的。有些是紧密耦合的,就像工厂的流水线,线程必须步调一致地工作。如果流水线中的一个线程被调度器暂停,那么该流水线中的所有其他线程最终都会在同步 ​​屏障​​ (barrier) 处停顿等待。为了解决这个问题,调度器可以使用 ​​组调度​​ (gang scheduling),即属于同一个并行“组”的所有线程作为一个单元被一起调度和抢占。这确保了当一个线程在运行时,它的同伴们也在运行,从而使整个并行任务能够取得进展。

一个更深层次的复杂性源于现代硬件的物理结构。在一台大型服务器中,一个 CPU 访问其自身插槽上的内存,比访问机器另一端另一个 CPU 插槽上的内存要快得多。这被称为 ​​非统一内存访问 (Non-Uniform Memory Access, NUMA)​​。这给调度器带来了一个可怕的两难困境。假设一个线程被调度在 CPU 1 上,但它的所有数据都驻留在 CPU 8 的内存中。它会因为远程内存访问而运行缓慢。调度器应该怎么做?

  1. 把它留在原地,接受性能下降。
  2. 将线程迁移到 CPU 8,那里它的内存是本地的。这听起来很棒,但迁移不是免费的。它有显著的一次性成本,用于传输线程的状态并在新 CPU 上预热缓存。

最优决策要求调度器是智能的。它必须比较在任务剩余的整个持续时间内缓慢运行的成本,与为了全速运行而支付前期迁移成本的利弊。因此,NUMA 系统上的调度器必须能够感知机器的拓扑结构,通过对减速因子 (σi\sigma_iσi​)、迁移成本 (rir_iri​) 和剩余工作量 (wiw_iwi​) 建模来做出正确的选择。

当优先级出错:反转的危险

对于实时系统,例如飞机上的飞行控制软件或发电厂的控制系统,公平性远不如绝对、可预测的优先级重要。高优先级任务 必须 在低优先级任务之前运行。但这个简单的规则可能被以微妙的方式颠覆,导致一种称为 ​​优先级反转​​ 的危险状况。

经典例子涉及三个线程:一个高优先级的 THT_HTH​,一个中优先级的 TMT_MTM​,和一个低优先级的 TLT_LTL​。想象一下,TLT_LTL​ 获取了一个共享锁(一个互斥锁,mutex)。然后,THT_HTH​ 变为就绪状态并抢占了 TLT_LTL​。THT_HTH​ 试图获取同一个锁,发现锁被持有,于是阻塞。现在,谁能运行?不是 THT_HTH​(它被阻塞了),也不是 TLT_LTL​(它被 TMT_MTM​ 抢占了)。于是,TMT_MTM​ 运行。结果是灾难性的:一个高优先级任务实际上被一个中优先级任务阻塞了。

解决方案是 ​​优先级继承协议 (Priority Inheritance Protocol, PIP)​​。当 THT_HTH​ 因等待 TLT_LTL​ 持有的锁而阻塞时,系统会临时将 TLT_LTL​ 的优先级提升到与 THT_HTH​ 相等。现在,即使有 TMT_MTM​ 存在,TLT_LTL​ 也可以运行。它可以快速完成其临界区,释放锁,然后其优先级恢复正常。现在锁已空闲,THT_HTH​ 可以获取它并继续其重要工作。如果有多个高优先级线程在等待由单个低优先级线程持有的锁,那么该低优先级线程将继承它所阻塞的所有线程中的 最高 优先级。

但优先级反转是一个九头蛇般的怪物。它不仅仅发生在锁上。考虑一个带有按需分页的系统,其中内存页可以被“换出”到磁盘。我们同样有这三个线程。THT_HTH​ 开始运行,但它需要的代码在磁盘上——发生了缺页。操作系统向交换设备发出一个 I/O 请求。但如果 I/O 队列已经充满了来自低优先级后台进程 TLT_LTL​ 的请求,而 I/O 调度器是一个简单的先进先出(FIFO)队列,会发生什么?THT_HTH​ 必须等待 TLT_LTL​ 的所有 I/O 完成。当 THT_HTH​ 因 I/O 而阻塞时,我们的中优先级朋友 TMT_MTM​(其内存都在物理内存中)则愉快地在 CPU 上运行。这同样是优先级反转,但现在的“锁”是一个 I/O 通道。

这里的解决方案与互斥锁的情况类似:

  1. ​​预防问题​​:​​钉住​​ (Pin) THT_HTH​ 的关键内存页,使它们永远不会被换出。没有缺页就意味着没有阻塞,也就没有反转。
  2. ​​解决问题​​:让 I/O 调度器具有优先级感知能力。当一个来自高优先级线程的缺页请求到来时,它的 I/O 请求应该跳到队列的前面。

这揭示了一个深刻的统一性:优先级反转是一个普遍的资源争用问题。无论资源是软件锁还是硬件设备,如果其分配策略不具备优先级感知能力,它就可能颠覆 CPU 调度器的整个优先级方案。

最后,我们必须认识到调度器能力的局限。它可以为线程提供运行的机会,但不能保证其进展。在一个著名的思想实验中,一个不公平的调度器可以反复在恰当的时机调度一个线程(T1T_1T1​),以确保它总能在争夺锁的竞赛中击败另一个线程(T2T_2T2​),导致 T2T_2T2​ 无限期地饿死。这并不违反内存一致性——所有内存操作的顺序都是正确的——但它破坏了活性。调度提供的是机会,而非命运。这催生了 ​​非阻塞算法​​ 的设计,这些算法通过巧妙地使用原子操作(如“比较并交换”),即使在面对不友好的调度器时也能保证进展。操作系统调度器策略与应用程序核心算法之间的这种深层联系,正是现代并发系统真正复杂与美妙之所在。

应用与跨学科联系

想象一个管弦乐队。指挥家的角色不仅仅是开始和停止音乐。他的职责是从每位音乐家那里引出最佳表现,确保小提琴不会淹没长笛,在精确而戏剧性的时刻引入打击乐,并将数十个独立的部分编织成一个令人叹为观止的整体。指挥家管理着时机、优先级和协调。操作系统的线程调度器就是我们数字管弦乐队的指挥家。在上一章中,我们考察了乐器和基本乐谱——线程、抢占和队列的原理。现在,我们将看到指挥家的实际行动,探索线程调度的艺术与科学如何远远超出内核的范畴,塑造着从单个硅芯片的性能到关乎生命的无人机的可靠性,乃至科学发现的根基。

杂耍的艺术:优化通用计算

从本质上讲,典型计算机上的调度是一场精湛的杂耍表演。一个现代系统运行着成百上千个线程,但它只有少数几个核心——可用来做功的“手”。许多线程用于你正在积极使用的应用程序,而其他线程则是后台系统任务。关键在于,许多应用程序线程并非总是在计算。你网页浏览器中的一个线程可能会发出一个请求,从服务器获取图像;当它等待数据通过网络传输时,它处于“阻塞”状态,实际上是在休眠。

这是调度器的绝佳机会。它不会让核心在线程等待时空闲,而是迅速进行上下文切换,转到另一个准备好计算的线程。但这种杂耍是有代价的。每次上下文切换都需要时间和精力。如果我们创建了太多的线程,调度器可能会把所有时间都花在换入换出上,这种现象称为颠簸(thrashing),即管理工作的“开销”压倒了有用的工作本身。

那么,正确的线程数量是多少?是否存在一个神奇的“超额订阅因子”——线程与核心的比率——能够最大化吞吐量?事实证明,答案可以通过数学建模以惊人的准确性估算出来。通过考虑线程因等待 I/O 而阻塞的概率以及上下文切换的成本,我们可以推导出保持核心繁忙而又不会因过多开销而崩溃的最优线程数。对于一个 I/O 频繁的系统,理想的线程数通常略多于核心数。这确保了当一个线程阻塞时,很可能有另一个线程准备好取而代之。这个简单的原理对于调整数据库、Web 服务器以及任何混合了重计算与 I/O 操作的应用的性能至关重要。

与硬件对话:具有机械共鸣的调度

一位真正伟大的指挥家了解音乐厅独特的声学效果和每种乐器的物理限制。同样,一个复杂的调度器必须具有“机械共鸣”——对底层硬件架构的感知能力。在现代机器中,天真地将所有核心视为相同且可互换的,是浪费性能的根源。

从核心到插槽:缓存和 NUMA 效应

让我们聚焦于硬件。一个线程不仅仅是在执行指令;它还在不断地访问数据。为了加速这一过程,每个 CPU 核心都有自己的一小块、速度极快的内存,称为缓存。当一个线程在一个核心上运行时,它会用自己频繁需要的数据来“预热”缓存。现在,如果调度器决定将该线程移动到另一个核心会怎样?该线程到达一个“冷”缓存,必须再次从主存中缓慢地获取其所有数据,而主存的速度要慢几个数量级。这就是为什么高性能应用常常使用 ​​线程亲和性​​(thread affinity)或“核心绑定”(pinning),将一个线程锁定到特定的核心,以确保其缓存保持温暖和就绪状态。

在拥有多个物理处理器或“插槽”的大型服务器上,情况变得更加复杂。每个插槽是一个包含一组核心的芯片。虽然一个插槽上的线程 可以 访问连接到另一个插槽的内存,但这种跨插槽链路的访问速度要慢得多。这被称为非统一内存访问(NUMA)架构。一个“感知 NUMA”的操作系统调度器会尝试将相互通信的线程放置在同一个插槽上。它还会尝试在线程可能使用的内存所在的插槽上启动该线程。性能增益可能是巨大的。一个不感知 NUMA 的调度器,如果随机地将两个通信线程放置在不同的插槽上,可能会因为它们等待数据跨越系统而引入大量停顿,而一个感知型调度器将它们共同定位,则会看到性能飙升。

深入核心:SMT 和 GPU 线程束

与硬件的对话可以更深入,直至单个核心的层面。许多现代 CPU 具有 ​​同步多线程(Simultaneous Multithreading, SMT)​​ 技术,以 Intel 的超线程(Hyper-Threading)技术而闻名。SMT 将一个物理核心作为两个(或更多)逻辑核心呈现给操作系统。其思想是,单个线程很少有足够的独立指令来让核心的所有执行单元在每个周期都保持繁忙。通过运行两个线程,硬件自身的内部调度器有更大的指令池可供选择,从而填补空隙并提高整体吞吐量。然而,这两个线程仍在竞争相同的有限资源。找到最佳的活动线程数是一个微妙的平衡:太少,核心会“挨饿”;太多,管理它们的开销会扼杀性能。

这种细粒度调度的舞蹈在图形处理单元(GPU)中达到了顶峰。GPU 以称为“线程束”(warps)的组来执行线程。在旧的架构上,一个线程束中的所有线程都以完美的步调一致执行,这是一种隐式的同步形式。然而,现代 GPU 允许在一个线程束内进行 ​​独立线程调度​​,以更好地隐藏内存延迟。这一变化打破了许多依赖于隐式步调一致的旧算法。程序员现在必须使用显式的线程束级屏障,实质上是告诉一组线程,“每个人都完成当前任务并在此等待,然后任何人才能继续。” 这确保了,例如,所有生产者线程在任何消费者线程开始读取共享内存之前都已将其数据写入共享内存。

超越速度:正确性、公平性与可预测性

虽然调度的许多方面都关乎最大化速度,但其一些最深刻的应用领域却将原始性能置于次要地位,而更看重其他更关键的保证。

实时系统:当截止时间就是法律

考虑一下四旋翼无人机的飞行计算机。它运行多个周期性任务:一个用于稳定姿态,一个用于读取传感器,一个用于控制电机。为了使无人机保持稳定,姿态控制循环必须(比如说)每秒执行 500 次,不能有任何失败。错过一个截止时间不是性能下降,而是一场灾难。

这就是 ​​实时系统​​ 的世界。在这里,使用像 ​​速率单调调度(Rate-Monotonic Scheduling, RMS)​​ 这样的调度算法。RMS 根据频率分配优先级:要求的速率越快,优先级越高。其目标不是最大化平均吞吐量,而是用数学确定性来证明,即使在最坏的情况下,每个“硬实时”任务也总能满足其截止时间。工程师使用这种分析来确定一个系统在违反其时序保证之前所能处理的最大计算负载,从而确保无人机——或你汽车里的防抱死制动系统——能够安全运行。

云中的公平性:用 cgroups 构建隔离墙

在现代云中,你的应用程序与来自数十个其他用户的应用程序一起在共享服务器上运行。是什么阻止一个失控的应用程序消耗所有 CPU 并使其邻居饿死?答案在于资源管理机制,如 Linux 的 ​​控制组(cgroups)​​。cgroup 可以被配置为将其中的应用程序限制在一组特定的 CPU 核心(一个 cpuset)和一定的 CPU 时间份额内。

这创建了一个虚拟容器,一个带墙的沙箱。然而,这些墙可能会“漏水”。一个引人入胜的现实世界问题是,当一个 cgroup 中的应用程序发起异步 I/O 时,内核可能会使用通用的“工作线程”来处理这个 I/O。如果这些工作线程不“感知 cgroup”,它们可能会在原始应用程序的 cpuset 之外运行,实际上是“逃离”了它们的容器,并窃取了完全另一个用户的 cgroup 的 CPU 周期。这打破了公平原则。解决方案需要修改操作系统,以确保代表某个 cgroup 完成的工作始终被计入并限制在该 cgroup 的墙内。这是容器化和云原生世界的调度基础。

控制尾延迟:分布式系统中的延迟

对于像 Google 搜索或 Facebook 动态消息这样的服务,平均响应时间很重要,但最不幸用户的体验可能更重要。这就是 ​​尾延迟​​ 问题——即第 99 或 99.9 百分位的响应时间。经历长时间延迟的用户是不满意的用户。这种延迟的一个主要来源是排队:当请求(或网络数据包)突然爆发时,它们可能会被卡在队列中,等待繁忙的 CPU。

线程调度提供了一个强大的工具来解决这个问题。通过为关键的、运行时间短的任务(如网络数据包处理)分配高优先级,调度器可以确保它们被立即处理,“插队”到较长的、不太关键的后台任务之前。这可以防止初始队列的累积。当然,给予某个任务绝对的优先级可能会饿死所有其他任务,因此这些高优先级的线程通常会被“限制”,以确保其使用的 CPU 不超过某个特定比例。这种对优先级和上限的谨慎使用对于构建稳定、低延迟的网络服务至关重要,使其即使在重负载下也能保持响应。

大统一:编译器、运行时和科学中的调度

调度的原理是如此基础,以至于它们以多种形式出现,甚至在操作系统之外。

运行时与垃圾回收

如果你曾用 Java、C# 或 Python 等托管语言编程,你很可能遇到过神秘的“GC 暂停”——你的应用程序似乎会冻结一瞬间。这是因为该语言的运行时有其自己的调度器:​​垃圾回收器(Garbage Collector, GC)​​。为了清理未使用的内存,许多 GC 会执行“stop-the-world”暂停,即暂停 所有 应用程序线程。在此暂停期间,GC 线程独占 CPU。你那高优先级的、I/O 密集型的应用程序线程,即使它的数据刚从网络到达,也仍然处于冻结状态。运行时的决策覆盖了操作系统调度器的优先级。理解这种交互作用是诊断和调优大量现代软件性能的关键。

科学计算中的可复现性

在一些最前沿的应用中,目标不是去适应调度器,而是要对其免疫。考虑一个依赖随机数的大规模科学模拟,比如蒙特卡洛模拟。为了使结果具有科学有效性,它们必须是可复现的。如果我们两次运行相同的模拟,我们必须得到完全相同的答案。

但是,在一个拥有数千个线程、运行在动态调度系统上的并行程序中,我们如何实现这一点呢?如果线程共享一个随机数生成器,它们访问它的顺序将是非确定性的,每次运行都会产生不同的结果。

解决方案是一次范式转换。我们不再生成一个由线程消费的单一随机数流,而是使用 ​​基于计数器的随机数生成器​​。这是一个无状态函数,它从一个逻辑“索引”生成一个随机数。我们将问题转化,使得例如我们模拟中第 iii 个粒子的工作直接从索引 iii 计算出它所需要的随机数。每个粒子的计算变得完全独立于所有其他粒子。哪个线程在何时做这项工作变得不再重要;结果保证是相同的。在这里,我们利用对依赖关系和调度的深刻理解,不是去管理它们,而是去彻底消除它们,从而实现了并行计算的圣杯:完美的、可复现的可扩展性。

从服务器农场的嗡嗡声到无人机的静默飞行,从网站的响应速度到科学结果的完整性,线程调度的微妙而强大的逻辑无处不在。它是无形的指挥家,将相互竞争的计算需求的嘈杂声,转变为一曲充满目标与力量的交响乐。