try ai
科普
编辑
分享
反馈
  • 多对多模型:一种并发与复杂性的通用模式

多对多模型:一种并发与复杂性的通用模式

SciencePedia玻尔百科
核心要点
  • 多对多线程模型通过将大量用户级线程映射到较小的内核级线程池上,在性能和并行性之间取得了平衡。
  • 该模型的两级调度系统可能导致优先级反转和信号丢失等复杂问题,需要在用户空间和内核空间之间进行精密的协调。
  • 在操作系统之外,多对多模式是在数据库、生物信息学和机器学习等领域管理复杂关系的一个基本概念。

引言

在对计算性能的不懈追求中,管理并发——即同时处理多项事务的艺术——成为一项核心挑战。简单的任务管理方法,例如为每个任务分配一个专用系统资源,或将所有任务汇集到单一资源上处理,都最终被证明要么成本过高,要么极其脆弱。这一差距凸显了对一种更精密、更均衡的并行性与效率方法的迫切需求。多对多模型正是在此背景下应运而生的一种优雅解决方案,它提供了一种强有力的折衷,其影响远远超出了其在计算机科学中的起源。

本文将分两部分探讨多对多模型。首先,在“原理与机制”一节中,我们将在操作系统线程的背景下剖析其内部工作原理,审视定义其强大功能的权衡之处,以及挑战其实现的微妙复杂性。然后,我们将在“应用与跨学科联系”一节中拓宽视野,揭示这一基本模式如何在不同领域中反复出现——从数据库架构、生物数据分析到现代神经网络的结构,从而展示其作为管理复杂性的通用工具所扮演的角色。

原理与机制

想象你正在经营一个大型车间。你有数百个小任务要完成——有些需要深度思考,有些则需要等待送货。你该如何组织你的员工队伍来高效地完成所有工作?这个简单的问题,本质上就是计算机并发性的根本挑战。这里的“任务”是你程序的逻辑执行线程,我们可以称之为​​用户级线程 (ULTs)​​。而“工人”则是计算机操作系统(即“政府”)实际知道如何调度到 CPU 核心上的实体;这些是​​内核级线程 (KLTs)​​。线程模型的艺术与科学,完全关乎你的任务与“政府”的工人之间的关系。

三种模型的故事:对并发的探索

最直接的方法是为每个任务都雇佣一个专属工人。这就是​​一对一模型​​。如果你有 1000 个任务,你就向操作系统申请 1000 个 KLT。这种方式非常简单和稳健。如果一个工人需要等待送货(即一次阻塞式 I/O 操作),“政府”只需派遣另一个工人到 CPU 核心上即可。车间永远不会停工。然而,雇佣和管理这些由“政府”批准的工人成本高昂。每次从一个工人切换到另一个工人时,都需要大量的文书工作(一次完整的内核上下文切换),这非常耗时。

于是,你可能会尝试相反的做法。如果你只雇佣一个超高效率的工人,并给他一份包含所有 1000 个任务的清单呢?这就是​​多对一模型​​。你的单个 KLT 在内部处理所有的 ULT。由于“政府”不参与,任务之间的切换速度极快,就像翻阅待办事项清单一样。但这个模型隐藏着一个致命缺陷。当你的唯一工人打电话并被置于等待状态时会发生什么?(这类似于一次​​阻塞式系统调用​​,比如从网络套接字读取数据。)因为你只有一个工人,所有事情都会停止。所有 1000 个任务都被冻结,等待那个电话结束。这可能导致你的车间陷入完全的死锁,这是一种灾难性的失败,如果没有外部干预,可能无法恢复。

这个两难困境——一对一模型的高成本与多对一模型的脆弱性——迫切需要一个更精细的解决方案。我们需要一种方法,既能获得多个工人的并行能力,又不必为每个任务支付全部的管理成本。

黄金分割点:多对多模型的承诺

于是​​多对多模型​​登场了。在这种模式下,你雇佣一个由 MMM 个内核级线程组成的小型受控团队,来执行你那更为庞大的、包含 NNN 个用户级线程的任务列表。这就是黄金分割点。你获得了真正的并行性,因为你的 MMM 个工人团队可以同时在 MMM 个不同的 CPU 核心上运行。同时,你又保留了用户级管理的效率。在每个 KLT 内部,你自己的内部“工头”——一个​​用户级调度器​​——可以快速地在分配给它的 ULT 之间进行切换,而无需打扰操作系统。

用户级调度器正是奇迹发生的地方。它是你程序内部的一段代码,有能力且有智能地根据你的特定工作负载做出决策。例如,你的“工头”可能会注意到,一些任务是“思考者”(CPU 密集型),而另一些是“呼叫者”(I/O 密集型)。一个聪明的“工头”可以将所有“呼叫者”分配给一两个专职工人,而让“思考者”独占其他工人。这种分离可以通过增强​​缓存局部性​​来显著提高性能。当一个“思考者”在同一个 CPU 核心上反复运行时,其所需的数据会保持在 CPU 的高速缓存中的“热”状态。而“呼叫者”会导致工人与内核通信并污染缓存,这种持续的中断得以避免。

雇佣多少工人(MMM)的选择不仅仅是猜测;这是一个可以进行数学分析的深刻权衡。想象一个工作负载,其中每个任务有 ppp 的时间比例在等待 I/O。在一对一模型中,每个等待中的任务都会释放一个 CPU 核心给另一个任务,但代价是高昂的内核上下文切换成本 (ckc_kck​)。在多对多模型中,上下文切换成本很低 (cuc_ucu​),但当一个工人因 I/O 而阻塞时,你实际上损失了一部分劳动力。存在一个临界阻塞分数 p∗p^*p∗,当阻塞分数低于此值时,多对多模型的效率更高;高于此值时,一对一模型卓越的并行性则更优。“最佳”模型并非绝对的,它取决于工作的性质。

两级管理的威力与风险

用户级调度器的存在创建了一个两级治理体系:管理 KLT 的操作系统内核调度器,和管理 ULT 的用户级调度器。这种权力分立既是该模型最大优势的来源,也是其最令人头疼的复杂性的根源。两个调度器通常不知道对方的意图,这会导致一些微妙且反直觉的问题。

沟通鸿沟

内核如何将重要事件(如某个工人因阻塞调用而卡住)通知用户级调度器?一个名为​​调度器激活 (Scheduler Activations)​​ 的巧妙方案被提了出来。其思想是,当一个 KLT 在内核中阻塞时,内核不仅仅让该 CPU 资源闲置。相反,它会通过一个新的、干净的 KLT 向用户级调度器发送一个“上调 (upcall)”,通知它发生了该事件。这使得用户级调度器能够立即将一个新任务分配给这个替代工人,从而保持并行性。

然而,这种持续的通信渠道本身也可能成为一个瓶颈。如果任务阻塞得非常频繁,内核和用户级调度器可能会将大部分时间都花在来回发送消息(上调)上。在高 I/O 场景中,这种​​上调开销​​可能会消耗 CPU 的大部分算力,几乎没有时间留给实际工作。此外,内核提供替代工人的承诺并非绝对。如果整个系统都很繁忙,它可能无法提供,从而导致并行性的暂时丧失。

优先级不匹配的暴政

一个更隐蔽的问题是​​优先级反转​​。想象一下,你的用户级调度器知道 ULT UHU_HUH​ 是你的最高优先级任务。它正在 KLT K1K_1K1​ 上运行。另一个不重要的任务 ULU_LUL​ 持有 UHU_HUH​ 需要的一个锁,而 ULU_LUL​ 正在另一个 KLT K2K_2K2​ 上运行。现在,假设操作系统内核出于自身原因,认为 K1K_1K1​ 的优先级远低于某个正在忙于运行中等优先级任务 UMU_MUM​ 的工人 K3K_3K3​。内核不知道你的应用程序内部正在上演的这出戏,它会抢占 K1K_1K1​,阻止 UHU_HUH​ 运行。更糟糕的是,它还可能抢占 K2K_2K2​,阻止 ULU_LUL​ 释放 UHU_HUH​ 所需的锁!结果是:你最重要的任务被无限期地拖延,不是因为持有锁的低优先级任务,而是因为一个不相关的中等优先级任务。

这种情况的发生是因为内核优先级和用户级优先级是解耦的。解决方案需要一种更复杂的协作:要么用户级调度器必须能够将持有锁的任务 (ULU_LUL​) 迁移到高优先级的 KLT 上,要么必须让内核意识到这种依赖关系,并临时提升持有锁的 KLT 的优先级。如果没有这样的机制,在一个严格的两级优先级系统中,即使是高优先级的任务也确实有可能发生饿死。

信号丢失

沟通鸿沟还以其他一些微妙的方式表现出来。如果内核在 I/O 操作完成时向进程发送一个“唤醒”信号,但此时你所有的 KLT 都在忙碌并且暂时屏蔽了该信号,而在此期间有十个 I/O 操作相继完成,会发生什么?在 POSIX 系统中,标准的非实时信号可以​​合并​​;内核看到了十个事件,但当一个 KLT 终于准备好监听时,可能只传递一个信号。九个唤醒信号丢失了,九个本应可运行的 ULT 将永远处于休眠状态。

即使是像错误码这样简单的事情也变得复杂起来。在传统编程中,当一个系统调用失败时,错误码会存储在一个类似全局变量的 errno 中。在多线程世界里,errno 必须是每个线程局部的。但到底是哪个线程呢?是执行调用的 KLT,还是请求调用的 ULT?如果一个 ULT 请求的调用在 KLT K1K_1K1​ 上失败了,然后在它检查错误之前,调度器立即将其迁移到 K2K_2K2​ 上运行,它可能会读取到 K2K_2K2​ 上下文中的 errno,这个值可能是零,或者更糟,是完全另一个错误的结果。一个健壮的多对多运行时系统必须在系统调用之后、在任何迁移发生之前,立即细致地从 KLT 的上下文中保存 errno,并将其附加到 ULT 的上下文中。

硅片中的回响:从线程到硬件

选择线程模型的后果不仅限于软件层面;它们的影响会一直波及到硬件本身的行为。

以​​地址转换后备缓冲器 (TLB)​​ 为例,这是 CPU 上的一个特殊缓存,用于存储从虚拟内存地址到物理内存地址的近期转换记录。当一个进程改变其内存映射(例如,通过卸载一个共享库)时,所有正在运行该进程线程的 CPU 核心都必须被告知使其过时的 TLB 条目无效。这是通过一个名为 ​​TLB 刷落 (TLB shootdown)​​ 的中断性事件完成的,操作系统向所有受影响的核心发送中断,导致短暂的暂停。

在这里,线程模型直接影响了这个事件的“爆炸半径”。在一对一模型中,你的进程可能同时在许多核心上运行,因此单个页表更改就可能触发大规模的刷落,暂停许多并发请求并放大尾延迟。在多对一模型中,进程一次只在一个核心上运行,因此刷落仅限于该核心。多对多模型则介于两者之间:受影响的核心数量受限于 KLT 的数量(MMM),而不是 ULT 的总数(NNN)。

类似地,当我们试图使用标准的​​采样分析器​​来观察程序性能时,多对多模型的两级结构就像一件隐形斗篷。作为操作系统的一部分,分析器只能看到 KLT。它将所有 CPU 时间都归因于采样时正在运行的 KLT。如果许多 ULT 在单个 KLT 上快速切换,分析器的报告将只显示一个非常繁忙的 KLT,而无法揭示真正的罪魁祸首是哪些底层 ULT。为了实现正确的归因,运行时系统必须进行插桩,以“揭示”当前运行的 ULT,提供元数据,使分析器能够将样本与逻辑任务关联起来,而不仅仅是与操作系统工人关联。

多对多模型是系统设计优雅与复杂性的一个明证。它是一种强大的折衷方案,既提供了效率又提供了并行性,但它的力量源于一种用户级的智能,这种智能必须在一个充满微妙风险的环境中航行——从优先级反转到信号丢失。它告诉我们,并发不仅仅是同时做很多事情,更是关于管理控制和通信的层级结构,这一教训在从公司结构到物理定律的万事万物中都能找到回响。

应用与跨学科联系

在探讨了多对多模型的原理之后,我们可能会倾向于将其局限于其诞生地:错综复杂的操作系统调度器世界。但这样做,就好比只通过观察苹果落地来研究万有引力定律。一个基本原理的真正美妙之处在于其普遍性——它以新的形式出人意料地在看似无关的领域中反复出现。多对多模型正是这样一个原理。它是一种铭刻在复杂信息本质中的模式,是解决连接两大组事物(其中链接错综复杂)问题的常见方案。现在,让我们踏上一段旅程,去探寻这一模式的各种形态,从高性能计算机的核心到生命的蓝图。

原生之地:高性能计算与操作系统

多对多模型最经典、最实实在在的应用就存在于你的计算机内部。现代软件渴求并发——即同时处理多项事务的能力。一个应用程序可能有成百上千个想要运行的任务,从更新用户界面到从网络获取数据。这些是它的“用户级线程”。然而,操作系统真正能同时运行的任务数量,取决于由“内核级线程”管理的 CPU 核心数量。挑战在于,如何将海量的用户任务映射到数量稀少而宝贵的内核执行资源上。

一对一映射,即每个用户任务都获得自己的内核线程,这种方式虽然简单,但极其浪费。内核线程是重量级的,消耗大量内存,并且当 CPU 在它们之间切换时会产生高昂的成本。在另一个极端,多对一映射,即将所有用户任务汇集到单个内核线程中,虽然是轻量级的,但很脆弱;如果任何一个任务执行了阻塞操作,比如等待文件加载,整个应用程序就会陷入停顿。

多对多模型是那个优雅的折衷方案。一个位于应用程序内部的用户级调度器,作为一名灵活的管理者,智能地将众多用户任务多路复用到一个较小的、可配置的内核线程池上。这种设计带来了巨大的优势。对于以 I/O 操作为主的应用程序——如 Web 服务器、数据库、以及持续等待网络的服务——它改变了游戏规则。想象一个每秒处理数百个请求的微服务,其中每个请求都涉及缓慢的 DNS 查询。如果每个请求都阻塞一个专用的内核线程,系统将需要数量庞大且笨重的线程。而在多对多系统中,当一个用户任务开始一个缓慢的网络等待时,用户级调度器可以简单地将其“停放”,并使用同一个内核线程去运行另一个准备好进行计算的任务。这种将计算与 I/O 等待重叠的能力是实现高吞吐量的关键,它允许少量内核线程(例如,每个 CPU 核心一个)为海量的并发操作提供服务。

然而,这种能力也伴随着其自身的微妙之处。该模型的性能取决于协作。如果少数用户任务变得“自私”——进行长时间、不间断的计算而不让出控制权——它们就会独占内核线程,导致所有其他任务饿死,甚至阻止调度器本身运行。这是一种“队头阻塞”,少数贪婪的任务可能会延迟数百个等待轮到自己的其他任务的处理。

当我们考虑到现代硬件的物理现实时,这个模型的复杂性就进一步加深了。计算机不再是一个单一的整体。多插槽服务器具有非统一内存访问 (NUMA) 特性,即 CPU 核心访问其自身插槽上的内存要比访问另一个插槽上的内存快得多。在这样的世界里,内核线程池不仅仅是一个抽象的集合;它们的物理位置至关重要。对于一个在多对多模型下运行的数据密集型应用,允许操作系统自由地将一个内核线程从一个 NUMA 节点迁移到另一个节点,可能会对性能造成灾难性影响,因为线程会突然发现其数据变得“遥远”。因此,高性能调优的艺术,就包括了告知系统如何以物理感知的方式管理其多对多映射:将内核线程绑定到特定节点,将网络中断引导至将要处理它们的核心,并仔细管理数据相对于处理它的线程的位置。

抽象模式:数据、知识与生物学

线程问题的结构——将一组用户任务映射到一组内核线程——并非操作系统所独有。它是一种用于关联任意两组条目的抽象模式。操作系统的“用户级调度器”在其他领域中找到了它的类似物,如“连接表”、“二元关系”或“中央清单”。

在最基本的层面上,这是一个来自数理逻辑的概念。假设我们想表示这样一个简单的事实:一本书可以有多个作者,一个作者也可以写多本书。我们无法用像 authorOf(book)authorOf(book)authorOf(book) 这样的函数来建模,因为一个函数必须返回一个单一的、唯一的值。这种关系在两端本质上都是复数的。在一阶逻辑中捕捉这一点的唯一方法是使用关系,即一个像 Authored(author,book)Authored(author, book)Authored(author,book) 这样的谓词,它只陈述一个特定的配对是否为真。这个二元关系就是多对多模型的抽象灵魂。

这个抽象思想在数据库世界中变得具体而至关重要。考虑在生物数据库中存储基因结构的挑战。一个基因可以被剪接成多个不同的信使 RNA 转录本,而基因的一个功能单元——外显子,可以被包含在许多不同的转录本中。为了表示这一点,我们不能简单地在“转录本”表中放置一个“外显子”列。我们需要一个独立的、专用的表——一个关联实体或连接表——其唯一目的就是存放配对:(transcript_id,exon_id)(transcript\_id, exon\_id)(transcript_id,exon_id)。这个 TranscriptExon 表是逻辑关系的物理化身。它是数据库版本的调度器任务列表,明确地存储了每一个有效的映射。此外,它还可以存储关系本身的属性,例如一个外显子在特定转录本中的排序,从而捕捉剪接的有序性。

这种模式不仅仅是为生物信息学家提供了便利;它本身就交织在生物学之中。自然界充满了多对多关系。当科学家使用蛋白质组学来识别细胞中存在哪些蛋白质时,他们面临着“蛋白质推断问题”。他们观察到的不是完整的蛋白质,而是更小的肽段。由于进化历史和可变剪接,一个单一的肽序列可能是多种不同蛋白质的片段。从观察到的肽段到推断出的蛋白质的映射是多对多的,这造成了一片模糊地带。这里的挑战不是设计关系,而是解开它。科学家们运用奥卡姆剃刀(简约性原则)等原理,来找出能够解释所有观察到的肽段的最小蛋白质集合,并发展出一套专门的词汇,如“剃刀肽”和“简并肽”,来管理这种自然形成的多对多映射所固有的不确定性。

我们自身科学知识的管理也依赖于这种模式。随着生物数据库的发展,基因标识符会发生变化。一个来自旧数据库的单一基因,在新数据库中可能被拆分为两个不同的基因。反之,几个旧的标识符也可能被合并成一个修正后的单一基因条目。跨越时间的标识符集合之间的映射是一个复杂的多对多图。分析这个图——计算稳定的一对一链接、一对多分裂、多对一合并以及复杂的纠缠关系——对于确保科学分析能够跨时间进行比较和重现至关重要。在当代的、整合了例如蛋白质组学和单细胞测序数据的多组学研究中,这种显式建模的需求至关重要。一个蛋白质组学测定可能来自多个生物样本的混合池。如果没有一个“中央清单”和一个“桥接表”来细致地记录这种多对多映射,研究人员可能会错误地将一个分子信号与错误的疾病状况关联起来,从而破坏整个科学事业的基础。

前沿领域:机器学习与序列建模

我们模式的探索之旅在当今最激动人心的领域之一——人工智能中达到了顶峰。在设计用于理解如 DNA 或人类语言等序列数据的神经网络时,多对多架构是一个基石。考虑这样一个任务:预测 DNA 序列中每个碱基对的突变风险。在这里,输入是一个包含许多项目的序列(碱基),输出是一个相应的包含许多预测的序列(风险)。

一种强大的方法是构建一个带有“共享主干”和多个“头”的循环神经网络 (RNN)。这个主干,一个由循环层组成的深层堆栈,读取整个序列并在每个位置上为其构建一个丰富的、具有上下文感知的表示。这类似于操作系统内核线程池提供一个通用的执行上下文。然后,可以附加一个“多对多头”,它使用每个位置的表示来进行局部的、特定位点的预测。同时,一个“多对一头”可以附加到主干的最终状态,对整个序列做出单一的、全局的预测(例如,“这段 DNA 是否包含与癌症相关的基序?”)。通过联合训练这两个头,来自局部和全局任务的误差信号都会回流到共享主干中,迫使它学习一个对两者都有用的表示。模型学会了关注特定的基序,因为它们对于局部和全局任务都很重要。

从操作系统调度器的务实平衡之举,到数据库模式的逻辑纯粹性,再到生物数据的令人困惑的模糊性,以及人工神经网络的预测能力,多对多模型证明了其价值。它证明了一个事实:在科学和工程领域,最优雅的解决方案往往是那些承认并拥抱世界固有复杂性的方案,它们提供一个框架,不是为了消除复杂性,而是为了清晰而有力地管理它。