流畅的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模块的内容不容易理解,需要查阅其他资料,另写一篇博文。
最新文章
- blog (后续更新)
- SpringNet学习笔记一
- ASP.NET MVC 教程-MVC简介
- IOS中取乱序数据最大值、最小值方法
- mysql root 密码丢失问题
- python中xrange和range的异同
- ansible自动化运维工具的安装与使用
- php spl
- asp.net解决高并发的方案.
- 浅谈c#枚举
- git checkout not discard changes
- 分享个新浪下载图片的ProgressBar进度样式
- shell 笔记
- ReactNative学习-webView
- Maven 搭建与my-app项目测试
- 开启Linux VNC远程桌面
- Python 第十二篇:HTML基础
- 如何查看centos系统版本
- think in uml 1
- 汉化Eclipse
热门文章
- 下载安装go插件包报错fatal: unable to access 'https://github.com/golang/tools.git/': OpenSSL SSL_read: SSL_ERROR_SYSCALL, errno 10054
- Django Ajax学习二之文件上传
- Table上下滚动
- AC日记——[LNOI2014]LCA bzoj 3626
- webStorm配置es6转es5
- H264与AAC ES打包成MP4
- Jsonp方式和httpclient方式有什么区别?
- python访问web的利器:urllib2
- STL容器 -- Stack
- Java 创建线程的方法