Async/Await 的设计空间探索
摘要
本文介绍了编程语言中直线异步性的设计空间探索,审视了不同语言中async/await实现的差异及其语义后果。
<p>摘要:许多现代编程语言都包含某种形式的异步编程。特别是,越来越多的语言现在拥有我们所称的直线异步性:试图提供与同步函数相似的异步函数,从而在不引入复杂控制的情况下实现异步。这些语言通常共享诸如“async”和“await”之类的构造名称,这暗示它们具有深层次的语义相似性。然而,仔细检查发现,这些语言在多个维度上存在显著差异,通常是微妙的。这些差异带来了真实的语义后果:看似相似的程序可能表现出不同的行为,让开发者和语言设计者都感到困惑。</p>
<p>因此,本文展示了直线异步性的设计空间探索。我们分析了几种现有语言,并展示它们在整体设计决策上如何不一致,这些决策影响执行的存在和顺序。我们阐述了一个包含九个维度的设计空间,覆盖异步计算的完整生命周期,涵盖诸如:语言在调用异步函数时提供什么确切的保证?任务生命结束时会发生什么?任务如何处理被取消的情况?我们通过具体例子、非正式设计讨论和形式化语义来探讨这些问题。我们的最终目标是帮助程序员、语言设计者和语言理论家更好地理解直线异步性的新兴格局。</p>
<p><a href="https://lobste.rs/s/4haxto/design_space_exploration_async_await">评论</a></p>
查看缓存全文
缓存时间: 2026/08/25 11:42
# 异步/等待的设计空间探索 来源:https://arxiv.org/html/2608.20677 \\correspondingauthor DOI:10\.1145/3839519 (https://doi.org/10.1145/3839519) 期刊:PACMPL 卷号:10 OOPSLA238710oopslab26main\-p1146\-p CCS:计算理论 程序语义 CCS:计算理论 并发 作者:Shriram Krishnamurthi https://orcid.org/0000-0001-5184-1975 机构:布朗大学,美国普罗维登斯 邮箱:[shriram@brown\.edu](mailto:[email protected]) 作者:Will Crichton https://orcid.org/0000-0001-8639-6541 机构:布朗大学,美国普罗维登斯 邮箱:[will\_crichton@brown\.edu](mailto:[email protected]) 收稿日期:2026\-06\-10 #### 摘要 许多现代编程语言都包含某种形式的异步编程。特别是,越来越多的语言现在拥有所谓的**直线型异步性**:试图提供看起来与同步函数相似的异步函数,从而在不引入复杂控制结构的情况下实现异步。这些语言通常共享“async”和“await”等构造名称,这暗示着它们具有深层的语义相似性。然而,仔细考察会发现这些语言在多个维度上存在显著差异,且往往十分微妙。这些差异会产生实际的语义后果:看似相似的程序可能表现出截然不同的行为,让开发者和语言设计者都感到困惑。因此,本文对直线型异步性进行了一次**设计空间探索**。我们剖析了几种现有语言,表明在影响执行存在与顺序的设计决策上,没有两种语言是完全一致的。我们构建了一个包含九个维度的设计空间,涵盖了异步计算的完整生命周期,涉及的问题包括:语言在调用异步函数时提供何种精确保证?任务结束时会发生什么?任务如何处理被取消的情况?我们通过具体示例、非形式化设计讨论以及形式化语义来探讨这些问题。我们的最终目标是帮助程序员、语言设计者和语言理论者更好地理解正在兴起的直线型异步性格局。 #### 关键词 异步编程,async/await,结构化并发 ††cc\-license:by ## 1\. 引言 软件开发者长期以来一直为编写既正确又高效的并发与并行程序而苦恼。编程语言通过逐步将并发元素从用户空间提升到语言设计层面来应对这一挑战。方法各异,但大多数现代语言共享的一个核心思想是**async/await**:将代码块标记为`async`,并使用`await`关键字等待异步块执行完毕。这些特性可在最新版本的Python、JavaScript、Swift、Rust、C\#等语言中找到。 我们研究async/await的初衷是帮助开发者将他们对这一概念的理解从一种语言迁移到另一种语言,例如从JavaScript到Rust。理论上,这两种语言包含相同的核心概念,只在细节上略有不同。我们应该能够像解释词法作用域变量、`if`语句或其他标准化语言特性一样解释async/await(Lu and Krishnamurthi, 2024 (https://arxiv.org/html/2608.20677#bib.bib21))。 然而现实中,async/await设计在表面上的相似性可能掩盖了深层的语义分歧。例如:协程、future、promise和task之间有何区别?当你取消一个task时会发生什么?task是否必须退出(如果需要的话)?错误如何跨越task边界传播(如果有的话)?对于具有可定制async/await运行时(例如Rust和Python)的语言,这些问题会因运行时选择的不同而变得更加复杂。 (代码块与图表略,保留格式结构) 图1\. 一种异步编程模式在七种现代async/await语言和运行时下表现出不同行为的示例。我们在第3节 (https://arxiv.org/html/2608.20677#S3)讨论了从伪代码到每种语言的转换过程。 作为一个具体例子,考虑图1 (https://arxiv.org/html/2608.20677#S1.F1)中的伪代码,它展示了运行后台任务以将信息写入日志的两种方式。语言在调用异步函数时发生的情况上存在差异,在task在其生成作用域结束时的行为上存在差异,在取消事件中task停止的时间与方式上也存在差异。当比较七种现代async/await方法时,**没有**两种语言在三个简单的调用上下文中表现出相同的行为! 观察到这种概念上的混乱,我们着手对现代语言中的async/await进行**设计空间探索**。本文的贡献正是这次探索的结果: 1. (1) 我们首先通过异步编程的高层概述来定位这项工作(第2节 (https://arxiv.org/html/2608.20677#S2))。我们对比了异步性与并发性和并行性,并介绍了常见的异步机制,如竞速和取消。 2. (2) 我们描述了现代语言中异步性的核心设计维度(第3节 (https://arxiv.org/html/2608.20677#S3))。我们的目标是捕捉设计中最根本的区别,重点关注那些导致看似相似的程序表现出不同行为的设计选择。 3. (3) 我们使用操作语义为每个设计维度提供了精确描述(第4节 (https://arxiv.org/html/2608.20677#S4))。我们以带分界符延续的核心异步演算的变体形式,给出了每种语言异步设计的精确模型。 ## 2\. 异步的核心概念 Async/await只是关键字——这些语言所表达的底层概念是什么?答案很明显,即**异步编程**,或更简单地说,**异步性**。如同并发和并行这些词一样,异步性并没有一个明确的定义。相反,它的含义是基于人们想要编写的程序类型而社会性地构建起来的。十五年前,异步编程被描述为“其特征在于对内部或外部事件的许多同时待处理反应”(Syme等人, 2011 (https://arxiv.org/html/2608.20677#bib.bib28))。今天,我们可能会补充:并行运行I/O密集型计算,或对计算的执行进行细粒度控制。异步编程的范围已经扩大。 此外,async/await只是异步性的一种形式。async/await的工作是避免与回调和事件循环相关的控制反转。正如C\#文档所说,其目标是“启用读起来像一系列语句,但以更复杂顺序执行的代码”(Wagner, 2025 (https://arxiv.org/html/2608.20677#bib.bib25))。async/await是关于依赖语言来简化控制流模式,而不是完全在库层面实现异步抽象。 在深入探讨异步设计维度的细节之前,我们首先提供2026年左右async/await的高层视图:异步性与并发性和并行性这些相似概念有何关系?如今编写async/await代码是什么样子? ### 2\. 1\. 异步 vs. 并发 vs. 并行 我们将**并发**定义为计算的一种逻辑属性,其中两个子计算可以交错执行:一个计算在另一个结束前开始。**并行性**是计算的一种物理属性,其中两个子计算可以同时执行:两个计算在同一时间点重叠。并行性意味着并发性,但反之不成立。例如,在单核上运行的嵌入式操作系统是并发的,但不是并行的。 我们认为**异步性**是**协作式并发**的一个特例。在异步程序中,两个子计算交错执行是因为它们被设计为相互让出控制权。异步性与**竞争式并发**形成对比,后者中的交错是通过外部强加发生的,例如通过上下文切换。竞争式并发通常出现在语言底层的架构层面,如操作系统线程和中断。 并发和并行有编程范式。并发计算的范式包括共享内存并发(例如pthreads)、通信顺序进程(MPI、Go)和Actor模型(Erlang)。并行计算的范式包括分叉-合并并行(Cilk、OpenMP)、模型并行(MapReduce、Spark)和数据并行(OpenGL、CUDA)。和所有范式一样,这些标签是有用但不完美的,例如一个复杂的并行程序通常会涉及共享内存并发。最终,一个范式是关于将一组相关机制(生成、互斥锁)聚焦于一组相关问题(对列表排序、运行数据库)。 异步性也有范式。**回调异步性**使用高阶函数为计算完成提供延续,最著名的是在C\#、JavaScript和Swift中。**事件循环异步性**围绕主循环组织程序,该循环对事件流分发反应,最著名的是在C\+\+和Rust中。语言历史上发展async/await是为了解决这两种异步范式相关的易用性问题。一个关键动机是为并发代码提供一种与非并发(同步)代码可比的表层语法。例如,以下是一些语言中async/await提案文档的摘录(更多示例见补充材料): > (C\#) “首先注意它与同步代码的相似性。……控制流完全未改变,看不到回调。这并不意味着没有回调,而是编译器负责创建并注册它们……” (Torgersen, 2010 (https://arxiv.org/html/2608.20677#bib.bib12)) > (Python) “提议将协程作为Python中的一个独立正式概念,并引入新的支持语法。最终目标是帮助在Python中建立一种常见的、易于理解的异步编程思维模型,并使其尽可能接近同步编程。” (Selivanov, 2015 (https://arxiv.org/html/2608.20677#bib.bib5)) > (Rust) “从用户的角度来看,他们可以像使用同步代码一样使用async/await,只需要为他们的函数和调用添加注解。” (withoutboats, 2018 (https://arxiv.org/html/2608.20677#bib.bib13)) ### 2\. 2\. 直线型异步性 因此,我们将async/await所体现的范式定义为**直线型异步性**,因其消除复杂控制流的基本组织原则。另一个名称是**任务异步性**,在C\#社区内使用。正如我们将讨论的,task确实是async/await的核心概念,但从跨语言的角度看,task并不是直线型异步性的唯一元素。例如,像Rust这样的语言提供协程,因此可以在不创建task的情况下实现直线型异步性。一些设计如Python\+Trio根本不向用户暴露任何task的概念。 本文的目标是探索直线型异步性的设计空间。我们主要关注那些(a)对于直线型异步性具有相对成熟设计,(b)使用相对广泛的语言。这包括具有单一异步形式的语言:JavaScript(具体为ECMAScript 2025,为清晰起见使用TypeScript类型)、C\#(\.NET 10上的v14)和Swift(v6\.2)的**结构化并发**子集。也包括具有模块化异步形式的语言:Python(v3.14,使用Asyncio和Trio v0\.33)和Rust(v1\.92\.0,使用Tokio v1\.50.0和Smol v2\.0\.2)。我们将在第6节 (https://arxiv.org/html/2608.20677#S6)讨论我们的发现与其他语言的关系。 在整个过程中,我们只考虑引入并发性的异步函数调用。我们不考虑立即等待的应用(例如`await f()`),它们在调用者恢复之前就已完成,因此在我们研究的每种设计下其行为都类似于同步调用。我们在第3节 (https://arxiv.org/html/2608.20677#S3)非形式化地描述我们的发现,在第4节 (https://arxiv.org/html/2608.20677#S4)形式化地描述。 我们不期望读者熟悉所有相关语言的具体语法,因此我们以高层形式阐述设计维度,并主要使用与图1 (https://arxiv.org/html/2608.20677#S1.F1)相同的伪代码风格呈现代码示例。然而,首先了解直线型异步性中语法、机制和语义的整体感觉和多样性是很有用的。因此,我们在本节结束时通过在多语言中实现一个异步工具来说明这一点。 (代码块略,保留格式结构) 图2\. 一个简化的`timeout`实现,展示了用于竞速的基本构造。超时的最小语义不保证超时的计算被取消,它仅保证执行不再被阻塞。 ### 2\. 3\. 超时案例研究 考虑实现一个`timeout`函数,它接受一个异步函数和一个时长作为输入。其最小语义是:`timeout`返回异步工作的结果,或者如果工作耗时超过时长,则返回一个表示超时的结果。`timeout`函数是异步编程的一个典型例子,因为仅使用其他并发或并行范式中的基本构造来实现它通常并不简单。 大多数直线型异步设计都包含一个**竞速**基本构造(也称为`select`、`or`、`wait`或`next`),它返回多个异步计算中第一个完成的。图2 (https://arxiv.org/html/2608.20677#S2.F2)展示了使用这些基本构造实现最小`timeout`语义的三种方式。 竞速的对立面是等待所有计算完成,分别称为`all`、`and`、`join`或`gather`。增强版的`timeout`语义应考虑**取消**...
相似文章
Tokio/Rayon 陷阱:为何 async/await 在并发中失灵
本文探讨了 async/await 语法虽然易于编写,但在生产环境中因将异步与并发混为一谈而引入了巨大复杂性,常需手动将 I/O 与计算任务分配到 Tokio 和 Rayon 等不同运行时,导致延迟飙升与系统不稳定。
异步编程的承诺与现实
深入剖析异步编程模型的演进——从回调到 Promise——揭示每一轮迭代如何解决先前的资源与性能问题,同时带来新的易用性挑战。
C++ futures 第三部分:std::async 的心智模型
Raymond Chen 解释 C++ std::async 的心智模型,说明 launch 策略的两个维度(立即/延迟、同线程/新线程)中哪些组合是标准允许的,哪些被禁止或由实现决定。
Lua-async
Lua-async 是一个为 Lua 语言增加异步编程功能的新库,在一个技术新闻平台上共享。
ClojureScript 迎来 Async/Await
ClojureScript 1.12.145 通过 ^:async 提示引入原生异步函数支持,实现与 JavaScript async/await 的直接互操作,无需额外依赖。