- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
18.2 使用 asyncio 和 aiohttp 包下载
从 Python 3.4 起,asyncio 包只直接支持 TCP 和 UDP。如果想使用 HTTP 或其他协议,那么要借助第三方包。当下,使用 asyncio 实现 HTTP 客户端和服务器时,使用的似乎都是 aiohttp 包。
示例 18-5 是下载国旗的 flags_asyncio.py 脚本的完整代码清单。运作方式简述如下。
(1) 首先,在 download_many 函数中获取一个事件循环,处理调用 download_one 函数生成的几个协程对象。
(2) asyncio 事件循环依次激活各个协程。
(3) 客户代码中的协程(如 get_flag)使用 yield from 把职责委托给库里的协程(如 aiohttp.request)时,控制权交还事件循环,执行之前排定的协程。
(4) 事件循环通过基于回调的低层 API,在阻塞的操作执行完毕后获得通知。
(5) 获得通知后,主循环把结果发给暂停的协程。
(6) 协程向前执行到下一个 yield from 表达式,例如 get_flag 函数中的 yield from resp.read()。事件循环再次得到控制权,重复第 4~6 步,直到事件循环终止。
这与 16.9.2 节所见的示例类似。在那个示例中,主循环依次启动多个出租车进程;各个出租车进程产出结果后,主循环调度各个出租车的下一个事件(未来发生的事),然后激活队列中的下一个出租车进程。那个出租车仿真简单得多,主循环易于理解。不过,在 asyncio 中,基本的流程是一样的:在一个单线程程序中使用主循环依次激活队列里的协程。各个协程向前执行几步,然后把控制权让给主循环,主循环再激活队列里的下一个协程。
下面详细分析示例 18-5。
示例 18-5 flags_asyncio.py:使用 asyncio 和 aiohttp 包实现的异步下载脚本
import asyncio import aiohttp ➊ from flags import BASE_URL, save_flag, show, main ➋ @asyncio.coroutine ➌ def get_flag(cc): url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp = yield from aiohttp.request('GET', url) ➍ image = yield from resp.read() ➎ return image @asyncio.coroutine def download_one(cc): ➏ image = yield from get_flag(cc) ➐ show(cc) save_flag(image, cc.lower() + '.gif') return cc def download_many(cc_list): loop = asyncio.get_event_loop() ➑ to_do = [download_one(cc) for cc in sorted(cc_list)] ➒ wait_coro = asyncio.wait(to_do) ➓ res, _ = loop.run_until_complete(wait_coro) ⓫ loop.close() ⓬ return len(res) if __name__ == '__main__': main(download_many)
❶ 必须安装 aiohttp 包,它不在标准库中。4
4可以使用 pip install aiohttp 命令安装 aiohttp 包。——编者注
❷ 重用 flags 模块(见示例 17-2)中的一些函数。
❸ 协程应该使用 @asyncio.coroutine 装饰。
❹ 阻塞的操作通过协程实现,客户代码通过 yield from 把职责委托给协程,以便异步运行协程。
❺ 读取响应内容是一项单独的异步操作。
❻ download_one 函数也必须是协程,因为用到了 yield from。
❼ 与依序下载版 download_one 函数唯一的区别是这一行中的 yield from;函数定义体中的其他代码与之前完全一样。
❽ 获取事件循环底层实现的引用。
❾ 调用 download_one 函数获取各个国旗,然后构建一个生成器对象列表。
❿ 虽然函数的名称是 wait,但它不是阻塞型函数。wait 是一个协程,等传给它的所有协程运行完毕后结束(这是 wait 函数的默认行为;参见这个示例后面的说明)。
⓫ 执行事件循环,直到 wait_coro 运行结束;事件循环运行的过程中,这个脚本会在这里阻塞。我们忽略 run_until_complete 方法返回的第二个元素。下文说明原因。
⓬关闭事件循环。
如果事件循环是上下文管理器就好了,这样我们就可以使用 with 块确保循环会被关闭。然而,实际情况是复杂的,客户代码绝不会直接创建事件循环,而是调用 asyncio.get_event_loop() 函数,获取事件循环的引用。而且有时我们的代码不“拥有”事件循环,因此关闭事件循环会出错。例如,使用 Quamash 这种包实现的外部 GUI 事件循环时,Qt 库负责在退出应用时关闭事件循环。
asyncio.wait(...) 协程的参数是一个由期物或协程构成的可迭代对象;wait 会分别把各个协程包装进一个 Task 对象。最终的结果是,wait 处理的所有对象都通过某种方式变成 Future 类的实例。wait 是协程函数,因此返回的是一个协程或生成器对象;wait_coro 变量中存储的正是这种对象。为了驱动协程,我们把协程传给 loop.run_until_complete(...) 方法。
loop.run_until_complete 方法的参数是一个期物或协程。如果是协程,run_until_complete 方法与 wait 函数一样,把协程包装进一个 Task 对象中。协程、期物和任务都能由 yield from 驱动,这正是 run_until_complete 方法对 wait 函数返回的 wait_coro 对象所做的事。wait_coro 运行结束后返回一个元组,第一个元素是一系列结束的期物,第二个元素是一系列未结束的期物。在示例 18-5 中,第二个元素始终为空,因此我们把它赋值给 _,将其忽略。但是 wait 函数有两个关键字参数,如果设定了可能会返回未结束的期物;这两个参数是 timeout 和 return_when。详情参见 asyncio.wait 函数的文档。
注意,在示例 18-5 中不能重用 flags.py 脚本(见示例 17-2)中的 get_flag 函数,因为那个函数用到了 requests 库,执行的是阻塞型 I/O 操作。为了使用 asyncio 包,我们必须把每个访问网络的函数改成异步版,使用 yield from 处理网络操作,这样才能把控制权交还给事件循环。在 get_flag 函数中使用 yield from,意味着它必须像协程那样驱动。
因此,不能重用 flags_threadpool.py 脚本(见示例 17-3)中的 download_one 函数。示例 18-5 中的代码使用 yield from 驱动 get_flag 函数,因此 download_one 函数本身也得是协程。每次请求时,download_many 函数会创建一个 download_one 协程对象;这些协程对象先使用 asyncio.wait 协程包装,然后由 loop.run_until_complete 方法驱动。
asyncio 包中有很多新概念要掌握,不过,如果你采用 Guido van Rossum 建议的一个技巧,就能轻松地理解示例 18-5 的总体逻辑:眯着眼,假装没有 yield from 关键字。这样做之后,你会发现示例 18-5 中的代码与纯粹依序下载的代码一样易于阅读。
比如说,以这个协程为例:
@asyncio.coroutine def get_flag(cc): url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp = yield from aiohttp.request('GET', url) image = yield from resp.read() return image
我们可以假设它与下述函数的作用相同,只不过协程版从不阻塞:
def get_flag(cc): url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower()) resp = aiohttp.request('GET', url) image = resp.read() return image
yield from foo 句法能防止阻塞,是因为当前协程(即包含 yield from 代码的委派生成器)暂停后,控制权回到事件循环手中,再去驱动其他协程;foo 期物或协程运行完毕后,把结果返回给暂停的协程,将其恢复。
在 16.7 节的末尾,我对 yield from 的用法做了两点陈述,摘要如下。
使用 yield from 链接的多个协程最终必须由不是协程的调用方驱动,调用方显式或隐式(例如,在 for 循环中)在最外层委派生成器上调用 next(...) 函数或 .send(...) 方法。
链条中最内层的子生成器必须是简单的生成器(只使用 yield)或可迭代的对象。
在 asyncio 包的 API 中使用 yield from 时,这两点都成立,不过要注意下述细节。
我们编写的协程链条始终通过把最外层委派生成器传给 asyncio 包 API 中的某个函数(如 loop.run_until_complete(...))驱动。
也就是说,使用 asyncio 包时,我们编写的代码不通过调用 next(...) 函数或 .send(...) 方法驱动协程——这一点由 asyncio 包实现的事件循环去做。
我们编写的协程链条最终通过 yield from 把职责委托给 asyncio 包中的某个协程函数或协程方法(例如示例 18-2 中的 yield from asyncio.sleep(...)),或者其他库中实现高层协议的协程(例如示例 18-5 中 get_flag 协程里的 resp = yield from aiohttp. request('GET', url))。
也就是说,最内层的子生成器是库中真正执行 I/O 操作的函数,而不是我们自己编写的函数。
概括起来就是:使用 asyncio 包时,我们编写的异步代码中包含由 asyncio 本身驱动的协程(即委派生成器),而生成器最终把职责委托给 asyncio 包或第三方库(如 aiohttp)中的协程。这种处理方式相当于架起了管道,让 asyncio 事件循环(通过我们编写的协程)驱动执行低层异步 I/O 操作的库函数。
现在可以回答第 17 章提出的那个问题了。
flags_asyncio.py 脚本和 flags.py 脚本都在单个线程中运行,前者怎么会比后者快 5 倍?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论