Brown大学的PL小组发表的论文《A Design Space Exploration of Async/Await》(Gavin Gray著)系统地审视了一个多数程序员视为已解决的特性:async/await。对于那些假设语义在各语言中基本相同的人来说,结论令人不安。一个在后台发起日志写入并睡眠一秒钟后打印"C"的程序,根据运行时的不同会产生至少四种不同输出——在该程序的三个变体上,七个被研究的运行时中没有任何两个在每一项输出上达成一致。

Python PEP 492文档页面,展示async/await规范的设计空间探索
Python增强提案492正式定义了async/await的语义,将原生协程与生成器分离,使Python运行时实现能够进行静态分析。peps.python.org

文章讨论了Python的PEP 492如何刻意将原生协程与生成器分离,使异步代码可静态区分并便于linter检查,同时承认不同运行时实现在易用性与语义控制之间的权衡。

论文将分歧归结为九个"设计维度",分为任务生命周期的三个阶段。生命周期起始维度决定异步调用如何求值——惰性求值(Python、Rust)、当前线程立即求值(C#、JavaScript),或因语言而异的挂起保证。生命周期结束维度涵盖任务范围(无界 vs. 限定于创建函数)、引用强度,以及任务如何被清理(等待、取消或直接终止)。取消维度——感知性、方向和持久性——决定任务能否以及如何响应取消。

两个维度解释了头条差异。动态范围语言(Swift、Python+Trio)不允许派生任务超出创建它的函数的生命周期。但在该范围结束时,Swift取消任务而Trio等待任务完成——因此同一源码在Swift上输出"AC",在Trio上输出"ABC"。论文用小步操作语义形式化了这些选择,精确追踪每个运行时的分叉点。

配套的语言规范确认这些选择是故意的而非偶然。Python的PEP 492将原生协程与生成器分离,使异步代码可静态区分并便于linter检查。Rust的async/await RFC(2394)将该特性视为future的语法糖,async函数返回一个匿名Future类型,仅在被轮询时执行。两份设计文档都承认该分类法所指出的权衡——易用性vs.控制力、性能vs.语义——且两者都未声称存在唯一正确的答案。

实际后果是:在一个运行时中正确工作的代码在迁移时可能改变行为,一种在某语言中看起来安全的"fire-and-forget"模式在另一种语言中可能泄漏、取消或阻塞。作者认为这些维度在原则上可逐轴配置,但实践中每种语言都选择空间中的一个点,知识无法干净地迁移——Rust RFC本身也承认这一担忧,指出完全可编程的轴使每个库都成为自己的异步DSL。