page contents

Python 使用 asyncio 包处理并 发(使用asyncio和aiohttp包下载)

从 Python 3.4 起,asyncio 包只直接支持 TCP 和 UDP。如果想使用 HTTP 或其他协议,那么要借助第三方包。当下,使用 asyncio 实现 HTTP 客户端和服务器时,使用的似乎都是 aiohttp 包。示例 18-5 是下载国旗的 flags_asyncio.py 脚本的完整代码清单。运作方 式简述如下。

attachments-2025-07-jNecs13268842da6a279c.jpg从 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 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 = yieldfrom aiohttp.request('GET', url) ➍

  image = yieldfrom resp.read() ➎

return image

@asyncio.coroutine

def download_one(cc): ➏

  image = yieldfrom 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 包,它不在标准库中.

❷ 重用 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(https://pypi.python.org/pypi/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 函数的文档 (https://docs.python.org/3/library/asyncio-task.html#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=c    c.lower())

  resp = yieldfrom aiohttp.request('GET', url)

  image = yieldfrom 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 倍?

更多相关技术内容咨询欢迎前往并持续关注好学星城论坛了解详情。

想高效系统的学习Python编程语言,推荐大家关注一个微信公众号:Python编程学习圈。每天分享行业资讯、技术干货供大家阅读,关注即可免费领取整套Python入门到进阶的学习资料以及教程,感兴趣的小伙伴赶紧行动起来吧。

attachments-2022-05-rLS4AIF8628ee5f3b7e12.jpg

  • 发表于 2025-07-26 09:22
  • 阅读 ( 34 )
  • 分类:Python开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1335 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 2172 文章
  3. Pack 1335 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章