Python 核心开发者 Nick Coghlan 在 2013 年 1 月对“PEP 3156—Asynchronous IO Support Rebooted: the‘asyncio’Module”(https://www.python.org/dev/peps/pep-3156/)草案评论如下:

在这个 PEP 的开头部分应该言简意赅地说明等待异步期物返回结果的两个 API:

(1) f.add_done_callback(...)

(2) 协程中的 yield from f(期物运行结束后恢复协程,期物要么返回结果,要么抛出合适的异常)

此刻,这两个 API 深埋在众多的 API 中,而它们是理解核心事件循环层之上各种事物交互方式的关键。15

15摘自 2013 年 1 月 20 日发布在 python-ideas 邮件列表中的一个消息(https://mail.python.org/pipermail/python-ideas/2013-January/018791.html),在这个消息中,Coghlan 对 PEP 3156 做出了上述评论。

PEP 3156 的作者 Guido van Rossum 没有采纳 Coghlan 的建议。实现 PEP 3156 的初期,asyncio 包的文档虽然十分详细,但对用户并不友好。asyncio 包的文档有 9 个 .rst 文件,128KB,将近 71 页。在标准库文档中,只有“Built-in Types”一章(https://docs.python.org/3/library/stdtypes.html)有这么长,而那一章内容众多,涵盖了数字类型、序列类型、生成器、映射、集合、bool、上下文管理器,等等。

asyncio 包的文档大部分是在讲概念和 API,其中夹杂着有用的示意图和示例,不过特别实用的一节是“18.5.11. Develop with asyncio”(https://docs.python.org/3/library/asyncio-dev.html),16 其中说明了极为重要的使用模式。asyncio 包的文档需要用更多的内容来说明如何使用 asyncio

16目前是:18.5.9. Develop with asyncio。——编者注

asyncio 包很新,已出版的书中少有涉及。我发现只有 Jan Palach 写的 Parallel Programming with Python(Packt 出版社,2014 年)一书中有一章讲到了 asyncio,可惜那一章很短。

不过,有很多关于 asyncio 的精彩演讲。我觉得最棒的是 Brett Slatkin 在蒙特利尔 PyCon 2014 大会上发表的演讲,题为“Fan-In and Fan-Out: The Crucial Components of Concurrency”(https://speakerdeck.com/pycon2014/fan-in-and-fan-out-the-crucial-components-of-concurrency-by-brett-slatkin),副标题是“Why do we need Tulip? (a.k.a., PEP 3156—asyncio)”(视频:https://www.youtube.com/watch?v=CWmq-jtkemY)。在 30 分钟内,Slatkin 实现了一个简单的 Web 爬虫示例,强调了 asyncio 包的正确用法。身为观众的 Guido van Rossum 提到,为了引荐 asyncio 包,他也写了一个 Web 爬虫。Guido 写的代码不依赖 aiohttp 包,只用到了标准库。Slatkin 还写了一篇见解深刻的文章,题为“Python's asyncio Is for Composition, Not Raw Performance”(http://www.onebigfluke.com/2015/02/asyncio-is-for-composition.html)。

Guido van Rossum 自己的几个演讲也是必看的,包括在 PyCon US 2013 上所做的主题演讲(http://pyvideo.org/video/1667/keynote-1),以及在 LinkedIn 公司(https://www.youtube.com/watch?v=aurOB4qYuFM)和 Twitter 大学(https://www.youtube.com/watch?v=1coLC-MUCJc)所做的演讲。此外,还推荐 Saúl Ibarra Corretgé的演讲——“A Deep Dive into PEP-3156 and the New asyncio Module”[(幻灯片(http://www.slideshare.net/saghul/asyncio),视频(https://www.youtube.com/watch?v=MS1L2RGKYyY)]。

在 PyCon US 2013 大会上,Dino Viehland 做了一场演讲,题为“Using futures for async GUI programming in Python 3.3”(http://pyvideo.org/video/1762/using-futures-for-async-gui-programming-in-python),说明如何把 asyncio 包集成到 Tkinter 事件循环中。Viehland 展示了在另一个事件循环之上实现 asyncio.AbstractEventLoop 接口的重要部分是多么容易。他的代码使用 Tulip 编写,这是 asyncio 包添加到标准库中之前的名称。我修改了他的代码,以便支持 Python 3.4 中的 asyncio 包。我重构后的新版在 GitHub 中(https://github.com/fluentpython/asyncio-tkinter)。

Victor Stinner [asyncio 包的核心贡献者,asyncio 包的移植版 Trollius(http://trollius.readthedocs.org)的作者 ] 经常更新相关资源的链接列表——“The new Python asyncio module aka‘tulip’”(http://haypo-notes.readthedocs.org/asyncio.html)。此外,收集 asyncio 资源的还有 Asyncio.org 网站(http://asyncio.org/)和 GitHub 中的 aio-libs 组织(https://github.com/aio-libs),在这两个网站中能找到 PostgreSQL、MySQL 和多种 NoSQL 数据库的异步驱动。我没有测试过这些驱动,不过写作本书时,这些项目好像十分活跃。

Web 服务将成为 asyncio 包的重要使用场景。你的代码有可能要依赖 Andrew Svetlov 领衔开发的 aiohttp 库(http://aiohttp.readthedocs.org/en/)。你可能还想架设环境,测试错误处理代码,在这方面,Alexis Métaireau 和 Tarek Ziadé开发的 Vaurien(“混沌 TCP 代理”,http://vaurien.readthedocs.org/en/1.8/)极其有用。Vaurien 是为 Mozilla Services 项目(https://mozilla-services.github.io/)开发的,用于在程序与后端服务器(例如,数据库和 Web 服务提供方)之间的 TCP 流量中引入延迟和随机错误。

杂谈

至尊循环

有很长一段时间,大多数 Python 高手开发网络应用时喜欢使用异步编程,但是总会遇到一个问题——挑选的库之间不兼容。Ryan Dahl 提到,Twisted 是 Node.js 的灵感来源之一;而在 Python 中,Tornado 拥护使用协程做面向事件编程。

在 JavaScript 社区里还有争论,有些人推崇使用简单的回调,而有些人提倡使用与回调处于竞争地位的各种高层抽象方式。Node.js 早期版本的 API 使用的是 Promise 对象(类似于 Python 中的期物),但是后来 Ryan Dahl 决定统一只用回调。James Coglan 认为,Node.js 在这一点上错过了大好良机(https://blog.jcoglan.com/2013/03/30/callbacksare-imperative-promises-are-functional-nodes-biggest-missed-opportunity/)。

Python 社区的争论已经结束:asyncio 包添加到标准库中之后,协程和期物被确定为符合 Python 风格的异步代码编写方式。此外,asyncio 包为异步期物和事件循环定义了标准接口,为二者提供了实现参考。

正如“Python 之禅”所说:

肯定有一种——通常也是唯一一种——最佳的解决方案

不过这并不容易找到,因为你不是 Python 之父

或许变成荷兰人才能理解 yield from 吧。17 对我这个巴西人来说,一开始并不易于理解,不过一段时间之后我理解了。

更重要的是,设计 asyncio 包时考虑到了使用外部包替换自身的事件循环,因此才有 asyncio.get_event_loopset_event_loop 函数——二者是抽象的事件循环策略 API(https://docs.python.org/3/library/asyncio-eventloops.html#event-loop-policies-and-the-default-policy)的一部分。

Tornado 已经有实现 asyncio.AbstractEventLoop 接口的类——AsyncIOMainLoophttp://tornado.readthedocs.org/en/latest/asyncio.html),因此在同一个事件循环中可以使用这两个库运行异步代码。此外,Quamash 项目(https://pypi.python.org/pypi/Quamash/)也很有趣,它把 asyncio 包集成到 Qt 事件循环中,以便使用 PyQt 或 PySide 开发 GUI 应用。我只是举两个例子,说明 asyncio 包能把面向事件的包集成在一起。

智能的 HTTP 客户端,例如单页 Web 应用(如 Gmail)或智能手机应用,需要快速、轻量级的响应和推送更新。鉴于这样的需求,服务器端最好使用异步框架,不要使用传统的 Web 框架(如 Django)。传统框架的目的是渲染完整的 HTML 网页,而且不支持异步访问数据库。

WebSockets 协议的作用是为始终连接的客户端(例如游戏和流式应用)提供实时更新,因此,高并发的异步服务器要不间断地与成百上千个客户端交互。asyncio 包的架构能很好地支持 WebSockets,而且至少有两个库已经在 asyncio 包的基础上实现了 WebSockets 协议:Autobahn|Python(http://autobahn.ws/python/)和 WebSockets(http://aaugustin.github.io/websockets/)。

“实时 Web”的整体发展趋势迅猛,这是 Node.js 需求量不断攀升的主要因素,也是 Python 生态系统积极向 asyncio 靠拢的重要原因。不过,要做的事还有很多。为了便于入门,我们要在标准库中提供异步 HTTP 服务器和客户端 API,异步数据库 API 3.0(https://www.python.org/dev/peps/pep-0249/),18 以及使用 asyncio 包构建的新数据库驱动。

与 Node.js 相比,含有 asyncio 包的 Python 3.4 最大的优势是 Python 本身:Python 语言设计良好,使用协程和 yield from 结构编写的异步代码比 JavaScript 采用的古老回调易于维护。而我们最大的劣势是库,Python 自带了很多库,但是那些库不支持异步编程。Node.js 库的生态系统丰富,完全建构在异步调用之上。但是,Python 和 Node. js 都有一个问题,而 Go 和 Erlang 从一开始就解决了这个问题:我们编写的代码无法轻松地利用所有可用的 CPU 核心。

Python 标准化了事件循环接口,还提供了一个异步库,这是一大进步,而且只有我们仁慈的独裁者能在众多深入人心且高质量的替代方案中选择这种方式。具体实现时,他咨询了多个重要的 Python 异步框架的作者,其中受 Glyph Lefkowitz(Twisted 的主要开发者)的影响最深。如果你想知道为什么 asyncio.Future 类与 Twisted 中的 Deferred 类不同,一定要阅读 Guido 在 Python-tulip 讨论组中发布的一篇文章,题为“Deconstructing Deferred”(https://groups.google.com/forum/#!msg/python-tulip/ut4vTG-08k8/PWZzUXX9HYIJ)。Guido 对 Twisted 这个最古老也是最大的 Python 异步框架充满敬意,在 python-twisted 讨论组中讨论设计方案时,他甚至说,“What Would Twisted Do(WWTD)”。19

幸好有 Guido van Rossum 打头阵,让 Python 以更好的姿态应对当前的并发挑战。若想精通 asyncio 包,一定要下一番功夫。可是,如果你计划使用 Python 编写并发网络应用,那就去寻求至尊循环(the One Loop):

至尊循环驭众生,至尊循环寻众生,

至尊循环引众生,普照众生欣欣荣。

17Python 之父 Guido van Rossum 是荷兰人。——译者注

18应该是:PEP 249—Python Database API Specification v2.0。——编者注

19出自 Guido 于 2015 年 1 月 29 日发布的消息(https://groups.google.com/forum/#!msg/python-tulip/pPMwts-CvUcw/eIoX_n8FSPwJ),然后 Glyph 立即回复了这一消息。