流畅的python第十八章使用asyncio包处理并发
对比一个简单的多线程程序和对应的 asyncio 版,说明多线程和异步任务之间的关
系
asyncio.Future 类与 concurrent.futures.Future 类之间的区别
摒弃线程或进程,如何使用异步编程管理网络应用中的高并发
在异步编程中,与回调相比,协程显著提升性能的方式
如何把阻塞的操作交给线程池处理,从而避免阻塞事件循环
使用 asyncio 编写服务器,重新审视 Web 应用对高并发的处理方式
为什么 asyncio 已经准备好对 Python 生态系统产生重大影响
线程与协程对比
import threading import itertools import time import sys class Signal: go = True def spin(msg, signal): write, flush = sys.stdout.write, sys.stdout.flush for char in itertools.cycle('|/-\\'): status = char + ' ' + msg write(status) flush() write('\x08' * len(status)) time.sleep(.1) if not signal.go: break write(' ' * len(status) + '\x08' * len(status)) def slow_function(): time.sleep(3) return 42 def supervisor(): signal = Signal() spinner = threading.Thread(target=spin, args=('thinking!', signal)) print('spinner object:', spinner) spinner.start() result = slow_function() signal.go = False spinner.join() return result def main(): result = supervisor() print('Answer:', result) if __name__ == '__main__': main()
以上是threading
import asyncio import itertools import sys @asyncio.coroutine def spin(msg): write, flush = sys.stdout.write, sys.stdout.flush for char in itertools.cycle('|/-\\'): status = char + ' ' + msg write(status) flush() write('\x08' * len(status)) try: yield from asyncio.sleep(.1) except asyncio.CancelledError: break write(' ' * len(status) + '\x08' * len(status)) @asyncio.coroutine def slow_function(): yield from asyncio.sleep(3) return 42 @asyncio.coroutine def supervisor(): spinner = asyncio.async(spin('thinking!')) print('spinner object:', spinner) result = yield from slow_function() spinner.cancel() return result def main(): loop = asyncio.get_event_loop() result = loop.run_until_complete(supervisor()) loop.close() print('Answer:', result) if __name__ == '__main__': main()
以上是asyncio
除非想阻塞主线程,从而冻结事件循环或整个应用,否则不要在 asyncio 协
程中使用 time.sleep(...)。如果协程需要在一段时间内什么也不做,应该使用
yield from asyncio.sleep(DELAY)
使用 @asyncio.coroutine 装饰器不是强制要求,但是强烈建议这么做,因为这样能在
一众普通的函数中把协程凸显出来,也有助于调试:如果还没从中产出值,协程就被垃圾
回收了(意味着有操作未完成,因此有可能是个缺陷),那就可以发出警告。这个装饰器
不会预激协程。
线程与协程之间的比较还有最后一点要说明:如果使用线程做过重要的编程,你就知道写
出程序有多么困难,因为调度程序任何时候都能中断线程。必须记住保留锁,去保护程序
中的重要部分,防止多步操作在执行的过程中中断,防止数据处于无效状态。
而协程默认会做好全方位保护,以防止中断。我们必须显式产出才能让程序的余下部分运
行。对协程来说,无需保留锁,在多个线程之间同步操作,协程自身就会同步,因为在任
意时刻只有一个协程运行。想交出控制权时,可以使用 yield 或 yield from 把控制权
交还调度程序。这就是能够安全地取消协程的原因:按照定义,协程只能在暂停的 yield
处取消,因此可以处理 CancelledError 异常,执行清理操作。
asyncio与concurrent.future的区别
期物只是调度执行某物的结果。在 asyncio 包
中,BaseEventLoop.create_task(...) 方法接收一个协程,排定它的运行时间,然后
返回一个 asyncio.Task 实例——也是 asyncio.Future 类的实例,因为 Task 是
Future 的子类,用于包装协程。这与调用 Executor.submit(...) 方法创建
concurrent.futures.Future 实例是一个道理。
与 concurrent.futures.Future 类似,asyncio.Future 类也提供了
.done()、.add_done_callback(...) 和 .result() 等方法。前两个方法的用法与
17.1.3 节所述的一样,不过 .result() 方法差别很大。
asyncio.Future 类的 .result() 方法没有参数,因此不能指定超时时间。此外,如果
调用 .result() 方法时期物还没运行完毕,那么 .result() 方法不会阻塞去等待结果,
而是抛出 asyncio.InvalidStateError 异常。
然而,获取 asyncio.Future 对象的结果通常使用 yield from,从中产出结果,如示例
18-8 所示。
使用 yield from 处理期物,等待期物运行完毕这一步无需我们关心,而且不会阻塞事件
循环,因为在 asyncio 包中,yield from 的作用是把控制权还给事件循环。
注意,使用 yield from 处理期物与使用 add_done_callback 方法处理协程的作用一
样:延迟的操作结束后,事件循环不会触发回调对象,而是设置期物的返回值;而 yield
from 表达式则在暂停的协程中生成返回值,恢复执行协程。
总之,因为 asyncio.Future 类的目的是与 yield from 一起使用,所以通常不需要使
用以下方法。
无需调用 my_future.add_done_callback(...),因为可以直接把想在期物运行结
束后执行的操作放在协程中 yield from my_future 表达式的后面。这是协程的一
大优势:协程是可以暂停和恢复的函数。
无需调用 my_future.result(),因为 yield from 从期物中产出的值就是结果
(例如,result = yield from my_future)。
当然,有时也需要使用 .done()、.add_done_callback(...) 和 .result() 方法。但
是一般情况下,asyncio.Future 对象由 yield from 驱动,而不是靠调用这些方法驱
动。
对协程来说,获取 Task 对象有两种主要方式。
asyncio.async(coro_or_future, *, loop=None)
这个函数统一了协程和期物:第一个参数可以是二者中的任何一个。如果是 Future
或 Task 对象,那就原封不动地返回。如果是协程,那么 async 函数会调用
loop.create_task(...) 方法创建 Task 对象。loop= 关键字参数是可选的,用于传入
事件循环;如果没有传入,那么 async 函数会通过调用 asyncio.get_event_loop() 函
数获取循环对象。
BaseEventLoop.create_task(coro)
这个方法排定协程的执行时间,返回一个 asyncio.Task 对象。如果在自定义的
BaseEventLoop 子类上调用,返回的对象可能是外部库(如 Tornado)中与 Task 类兼容
的某个类的实例。
使用 asyncio 包时,我们编写的异步代码中包含由 asyncio 本身驱动的
协程(即委派生成器),而生成器最终把职责委托给 asyncio 包或第三方库(如
aiohttp)中的协程。这种处理方式相当于架起了管道,让 asyncio 事件循环(通过我
们编写的协程)驱动执行低层异步 I/O 操作的库函数。
import asyncio import aiohttp from ..chapter17.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)
有两种方法能避免阻塞型调用中止整个应用程序的进程:
在单独的线程中运行各个阻塞型操作
把每个阻塞型操作转换成非阻塞的异步调用使用
现在你应该能理解为什么 flags_asyncio.py 脚本的性能比 flags.py 脚本高 5 倍了:flags.py
脚本依序下载,而每次下载都要用几十亿个 CPU 周期等待结果。其实,CPU 同时做了很
多事,只是没有运行你的程序。与此相比,在 flags_asyncio.py 脚本中,在
download_many 函数中调用 loop.run_until_complete 方法时,事件循环驱动各个
download_one 协程,运行到第一个 yield from 表达式处,那个表达式又驱动各个
get_flag 协程,运行到第一个 yield from 表达式处,调用 aiohttp.request(...)
函数。这些调用都不会阻塞,因此在零点几秒内所有请求全部开始。
asyncio 的基础设施获得第一个响应后,事件循环把响应发给等待结果的 get_flag 协
程。得到响应后,get_flag 向前执行到下一个 yield from 表达式处,调用
resp.read() 方法,然后把控制权还给主循环。其他响应会陆续返回(因为请求几乎同
时发出)。所有 get_ flag 协程都获得结果后,委派生成器 download_one 恢复,保存
图像文件。
因为异步操作是交叉执行的,所以并发下载多张图像所需的总时间比依序下载少得多。我
使用 asyncio 包发起了 600 个 HTTP 请求,获得所有结果的时间比依序下载快 70 倍。
关于concurrent.future模块以及asyncio模块的内容不容易理解,需要查阅其他资料,另写一篇博文。