目前,async/await 就是 Python 写异步代码的标准做法。短短几个关键字,把许多并发逻辑写得干净了。这件事看起来简单,但背后是一段长达二十年的演进史,先有生成器当“半调子协程”,再有人造的补丁方案,最后语言层面才把协程当成一等公民。

倒着说起。2015 年,PEP 492 把 async/await 扔进了语言。语义上它把“等待子任务完成”的场景做了专门化处理,写法更直观,也更严格:await 只能用在异步上下文里,目标必须是协程或兼容的 awaitable。配合 async/await 出现的,还有 asyncio 这个库生态逐步成熟起来,事件循环、任务调度、Future、Transport/Protocol 这些组件一并到位,使得原本分散的实现有了统一的运行时基础。代码能写得更小,错误处理也更明确。
回到 async/await 之前。2012 年,Python 3.3 引入了 yield from(PEP 380)。这个语法的出现解决了生成器嵌套时的一大痛点:子生成器的 send、throw、close 能被父生成器透明转发,子生成器 return 的值会被封装进 StopIteration.value 并返回给父层。用过的人都会觉得方便——父层不需要写一堆转发样板代码,控制权能够完整地交接给子生成器直到它结束。这个特性让用生成器写异步流程的表达力大幅增强。

在 yield from 出现之前,以及在它普及的早期阶段,社区里有人做了工程上的替代方案。OpenStack Heat 项目里有一个叫 @wrappertask 的装饰器,用来模拟 yield from 的委托语义。这个东西在 2013-05-07 被加入代码库,后面在 2023-12-27 被移除。看时间你会发现一个有意思的实际:wrappertask 是在 yield from 已有标准的情况下出现的。缘由也简单——许多生产系统还在跑 Python 2.7,那个时代的工程团队没法等语言升级,只能在应用层造补丁。wrappertask 的目标就是让一个生成器“启动”另一个生成器作为子任务,尽可能把异常、close 等控制信号在父子间传递。实现不算完美:像 send 值的透传、子任务 return 值的自然捕获这些地方还有短板,但在 Heat 那类复杂的资源编排场景里,它足够用。可以把它看作是对未来 yield from 语义的一次现实世界验证——用工程换经验。
再往前看,真正能让生成器变成协程关键的一步出目前 2006 年的 Python 2.5:生成器新增了 .send(value) 方法。这个特性改变了生成器的用途。早期的 yield 更像是增强版的 return,用来做节省内存的惰性序列。但当生成器能接收外部传入的值后,它就能保持内部状态、被外部“唤醒”并继续执行,这基本就是协程的本质:可中断、可恢复、能和调用方交互的执行体。代码里写出一个能接收输入的生成器,从逻辑上已经接近协程模型了。

在那之前,人们用生成器写复杂任务流时会遇到一个常见问题:在父生成器里“调用”子生成器,并让所有控制信号包括 send、throw、close 能正确传到子生成器,实际上很难做到。举个常见状况:父生成器用 yield 向外产出子任务结果,但外部若想向子生成器发数据,这会被父层挡在中间,父生成器既不是最终执行者,也没有天然的代理能力去把外部发来的值直接送进子生成器。如果外部抛出异常,父层也不知道该如何把异常传给子层;当子生成器通过 return 返回结果时,那个值被封在 StopIteration.value 里,父层要额外处理才能拿到。总之,没有一种干净的委托机制,嵌套生成器就会产生一堆样板代码和边界问题。
从历史看,几个阶段是逐步堆叠起来的。最早在 2001 年,Python 2.2 引入了 yield,主要用来做惰性序列,解决大数据遍历时内存问题。那时并没人把它当成协程来用。接着是 send 的加入,把生成器的角色从“只输出值”变成了“能接收输入并维护状态的执行体”,这给协程思想埋下了伏笔。社区在实践中不断尝试,把生成器当作协程来写许多异步逻辑;遇到限制就用工程手段补,列如上面提到的 wrappertask;语言随后补上了 yield from,解决了生成器嵌套的信号转发问题;最后 PEP 492 把这些模型专门化成 async/await,搭配 asyncio,把运行时和语法统一起来。

在 OpenStack Heat 的例子里,wrappertask 的核心意图是让父任务可以把子任务当作一个整体去调度:父层 yield 子任务的一个迭代对象,装饰器在内部负责把从子任务抛出来的异常或关闭操作翻译并传递回来。实现起来比较琐碎,有许多边界要处理,像任务失败时的异常展开、任务被 cancel 时的清理逻辑等。它能支撑 Heat 的资源编排逻辑,是一个实际可用的工程方案,但它与语言层面的 yield from 在语义上还是有差距。
把时间线捋清楚,能看到一个常见工程规律:语言可能先有设计,生产环境却由于兼容性等缘由滞后一段时间。开发者会在不能等语言升级的现实下,做出各种补丁与中间产物。这些产物看似多余,却提供了宝贵的实践反馈,推动标准特性根据真实需求调整。用屋檐下的木板搭桥,等大桥造好再拆掉,这是周期的一部分。

关于 async/await 与之前方案的差别,细节层面值得注意:yield from 是通用的委托语法,能把所有生成器相关的控制操作原样转发;await 把焦点收窄到异步任务上,语义更适合事件驱动、Future/Task 这类对象。async/await 把语言和运行时的契合度做好了,从而减少了用户在写并发逻辑时必须手动处理的样板和陷阱。配合成熟的 asyncio,常见的并发场景——定时、IO、任务撤销、并发限制——都有了标准化的解决方案。
最后补一句个人见解:看到这些演进细节,会觉得许多美丽的语法背后实则是工程上的妥协和逐步优化,不是一次性想出来的完美设计。顺便说明一下背景:本文作者来自阿里云资源编排团队,团队主要做基础设施即代码的自动化部署,核心服务用 Python 构建,承载大量云资源编排场景。文中提到的实践和经验,都是在这些生产级别需求下积累出来的。文末还顺带提到一个相关产品:基于自研通义万相的 AIGC 图像生成功能,能把文本转图像、涂鸦转风格、人物写真等集成到 Web 服务里,能提高创作效率,相关技术方案在阿里云技术文档中有详细介绍。

