你以为懂 async/await?七种语言七种结果

A Design Space Exploration of Async/Await

你以为懂 async/await?七种语言七种结果

许多现代语言都提供了 async/await 关键字,旨在让并发代码看起来像顺序代码。但布朗大学的研究团队发现,不同语言对 async/await 的实现差异远超预期。他们通过一个简单的伪代码程序测试了七种现代运行时,结果竟出现了四种不同的输出。研究揭示了九个关键的设计维度,如任务的生命周期、取消机制和异常传播等,解释了为何 Swift 和 Trio 等语言在相同代码下表现迥异。没有绝对的对错,只有不同的设计权衡。这篇论文通过形式化语义模型,清晰地展示了这些设计决策如何影响程序行为,帮助开发者真正理解自己所用语言的异步语义。

你实际上真的了解你所在语言的 async 语义吗?
  1. biorach

    长期以来,async 运行时之间那些根本性的实现选择至关重要,这一点早已不言而喻,但竟然有 _九_ 个设计维度?天哪。

    我认为 async 具有欺骗性,因为它看起来像是语言中一个自包含且相对直白的部分。但实际上需要做出的设计选择很多,而且每一个都有深远的影响。

    此外,我认为其中许多维度的影响尚未被完全理解,我们整体上仍在努力理解它们在具体实现中是如何发挥作用的。再加上某些影响的微妙性质以及它们之间的组合……

    我觉得一个很好的类比是编程语言中的词法作用域与动态作用域之争。在设计编程语言的早期,这一设计维度争论了十几年甚至二十年。直到随着时间的推移,人们在处理具体实现中积累了经验,才逐渐明确词法作用域应作为默认选择,而动态作用域应仅限于各种特定场景。

  2. cpa

    这种以语义为首要关注点的编程语言对比分析至关重要。

    我在大学时上过一门课,深入剖析了不同语言如何处理并发、并行、模块/OOP、元编程、急切求值与惰性求值、类型、异常等问题。理解每种语言所做的权衡(及其历史渊源)让我对编程的理解远超任何 Python/Java/C 课程,也让我更容易掌握新语言。

  3. galaxyLogic

    我在 JS 中使用 async/await 时遇到的问题在于:虽然 async 方法可以调用非 async 函数并利用其结果,但反过来却不成立——同步函数可以调用 async 函数,却无法以任何方式使用其结果,除非将其传递出去或向上传递。

    更糟糕的是,如果一个同步函数已有调用者,你无法简单地将其修改为 async 函数,因为这很可能会破坏那些调用者。

    这会影响我程序中所有可能的函数调用树。如果在某个层级我有一个同步函数,却发现它需要获取只有 async 函数才能提供的数据,我可能需要修改整个调用链,而不仅仅是修改该调用树中的单个函数。

    随着我开发程序的进行,我不得不为每个函数做出决定:它应该是同步的还是异步的?在很多情况下,两者皆可,那我该选哪个?将其设为 async 似乎会让程序后期的演进更容易。但那样的话,把所有函数都设为 async 又有意义吗?

  4. spankalee

    哇,这真的很有帮助,而且来得正是时候!

    我正在开发一种带有 async/await 的新语言,不得不做出许多此类决策,但我没有一个如此条理清晰的框架来作为依据。很高兴看到我的选择主要基于 Trio,并带有一点 JavaScript 的影子。

    我的语言(Zena)的 async 文档页面:https://zena-lang.dev/guide/async/ 我想我会再通读一遍,尝试更明确地指出这些决策点。

    顺便一提,我发现 Trio 作者关于取消机制的这篇博文非常有说服力:https://vorpus.org/blog/timeouts-and-cancellation-for-humans... 我正是基于它来设计 Zena 的取消机制的。

    补充编辑:我确实希望这篇文章在“取消”部分能包含 JavaScript 的 AbortSignal。不是因为 AbortSignal 本身有多好,而是因为传递取消令牌(cancel tokens)是一种确实存在的模式。此外还有一个维度是关于谁可以发起取消,以及像 AbortSignal 那样,任务是否需要显式选择加入取消检查。

  5. theamk

    很棒的文章,但这个测验不太公平——它假设只有一种“正确的方式”,但实际上很多框架都提供了多种选项。

    例如,Trio 出于设计考虑,根本没有全局的 "spawn" 方法。根据结果来看,作者假设了 "with trio.open_nursery() as n: n.start_soon(write_to_log())" 这种写法,因此得出了急切执行、动态作用域和破坏性传播的结论。

    但仅仅为了写一行日志就打开一个 nursery 简直太疯狂了!真实的程序会使用适当作用域的共享 nursery:要么是每个请求一个,要么是全局的。后者允许无限的作用域和“永不”传播。

    此外,write_to_log 后面的 "()" 也很重要!如果遵循 Trio 自己的示例,你会写成 "n.start_soon(write_to_log)"——注意没有 ()。这将切换为惰性执行。

    我不熟悉列表中列出的非 Python 框架,但我不惊讶它们也能提供同样广泛的行为模式。

  6. jcelerier

    我本来在想:“希望 C++ 能让你在这些轴线上自由选择,从而构建出最适合手头问题的 async 原语”,然后答案是:是的!

    > 我们无法将 C++ 归入表 1 中提供的任何特定设计点,因为每个轴线都是可配置的。虽然这种完全可编程性既优雅又中立,但它使得每个库都变成了一个 async DSL;在同一语言的不同项目之间进行知识转移变得极其困难。

    关键在于,你要思考的是这些轴线以及它们如何解决你的特定问题,而不是某种特定的具体设计。举个最简单的例子:一个网络视频播放器。例如,某个服务器发送 RTP 音频和视频帧给你,你需要正确播放它们,并在其上叠加一个漂亮的 GUI。

    如果你想以尽可能高效的方式实现这一点,你需要了解所有可能的 async 互操作方式:

    - 在经典的网络状态机中连接和接收数据包,这里协程表现优异

    - 处理视频帧显示时的 vsync 与非 vsync 模式

    - 遵循你所使用的硬件视频解码系统所提供的 async 范式,例如 Intel QuickSync、VideoToolbox 或 NVDEC 等

    - 处理由拉模式(pull mode)驱动的音频播放的同步模型

    - 处理音频/视频之间的同步,以及支持多线程的 async 模式,因为你的音频线程无法 […]

  7. flossly

    在我上一个项目(Kotlin;Web 应用)中,我决定不使用 async(协程)。

    我只想保持简单。

    Async 会“感染”你的代码:为了获得其带来的好处,你的整个代码库都需要采用它(包括请求摄入、数据库调用、Web API 调用等)。

    正因为如此,我们在编程语言中看到了“分裂”的栈:一套库栈用于同步,另一套用于异步。

    我认为在负载下获得更好性能的好处,并不值得付出在所有地方都使用 async 所带来的心智负担。所以我选择了阻塞调用和虚拟线程。毫无遗憾。

  8. gugagore

    > 对于“基础”抽象,lambda 演算是标准方法。但在并发领域没有等效的标准方法。存在多种不同的方法。哪一种是标准的?

    简单类型 lambda 演算保证了计算会终止。但有时你需要非终止。不动点算子(Fixed point operators)就是把你之前排除的东西重新引入进来的东西。

    线性逻辑(Linear logic)在处理那些你不需要并发的并发片段时很好。线性逻辑保证了没有竞态条件。但有时你需要竞态条件——你该怎么把它放进去?有时候,拥有竞态条件真的非常重要:我在卖票,肯定会有一场争夺最后一张票的竞赛。

    有没有一种单一的东西可以加到线性逻辑中,从而给我带来竞态条件?目前未知。

    - Philip Wadler 在 Type Theory Forall #54 - The Goal of Science is to Communicate Ideas! 中的发言

同日更多故事

2026-09-11