说明
异步调用真的挺容易忘的,而且忘的很彻底…
安利下自己的Python 全栈系列56 - asyncio的使用,看完以后帮我捡起了不少记忆。
本篇基于实践做一个尽量简单的示例。
内容
异步调用的本质是充分利用cpu,避免无谓的等待。
所以如果没有带宽、ip的限制,看着cpu还挺闲的,就应该使用异步方式获取(网络)数据。
两个主要概念:
- 1 Future: future是一个数据结构,表示还未完成的工作结果。事件循环可以监视Future对象是否完成。从而允许应用的一部分等待另一部分完成一些工作。
- 2 Task:task是Future的一个子类,它知道如何包装和管理一个协程的执行。任务所需的资源可用时,事件循环会调度任务允许,并生成一个结果,从而可以由其他协程消费。
简单的理解,Future就是经理(监工),他来调度CPU完成任务(Task),不让CPU闲着。但很显然,所有工人默认的方式是同步的(就是一个接一个),所以必须有机制进行区分和管理。
1 异步函数(任务)
python里默认定义的是同步函数:运行方式是一个接一个,asap
def func(xxx):
return xxx
先在对于异步的任务,我们使用异步函数的命名方法以示区分:
async def afunc(xxx):
return xxx
特别要注意的是,一旦决定要异步,那么async
里面的函数一定都是async + await
的,否则会变成看起来像异步的同步函数。
2 组织和运行
定义了异步函数是否就可以异步运行?fetch
函数是异步函数,但是直接用await
运行是没用的(看起来像异步的同步)。
事实上,一个异步函数不能自己「做主」,而是要向「经理」报到,表示自己是个异步函数,服从安排。然后再由经理(Future
)来把这个异步任务安插给CPU。(没领导写推荐信就得老老实实排队,就慢了,可以这么理解)
异步函数向经理「报到」有三种方式,简单起见,我就用gather
了,原因后面讲。
异步工作的本质是加快一维数组的循环过程
本来有1000个任务,大家老老实实的按顺序排队往里走,队伍排的很长。门口的老大爷比较悠闲的喊“~下一个”那种。
异步则是1000个人直接往里挤,只要有空隙就往里走,队伍和门一样宽。进门的时候老大爷直接眼神对一个就进去了。
3 实际使用
我有1亿个数据需要通过接口读取,传入id作为参数。
定义了任务类型fetchx
, 考虑到异步请求的结果会乱序,所以每次返回的是以x为键值的字典。
import aiohttp
import asyncio
import time
import json
async def fetchx(x,url_template):
async with aiohttp.ClientSession() as session:
async with session.get(url_template % x) as response:
res = await response.text()
return {x:json.loads(res)}
同步方式花费0.18秒(读3个)
异步方式需要让这些任务向「经理」报到,tasks
就像是名单。
doc_id_list = [10,20,30]
def make_async_task_list(doc_id_list, url_template):
tasks = []
for doc_id in doc_id_list:
task = asyncio.ensure_future(fetchx(doc_id, url_template))
tasks.append(task)
return tasks
---
start = time.time()
a = make_async_task_list(doc_id_list,url_template)
end = time.time()
print('共花费 %.6f' % (end-start))
---
共花费 0.000191
In [37]: a
Out[37]:
[<Task pending coro=<fetchx() running at <ipython-input-29-5811f27dc039>:6>>,
<Task pending coro=<fetchx() running at <ipython-input-29-5811f27dc039>:6>>,
<Task pending coro=<fetchx() running at <ipython-input-29-5811f27dc039>:6>>]
然后将「名单」交给「经理」
start = time.time()
res_list = await asyncio.gather(*a)
end = time.time()
print('共花费 %.2f' % (end-start))
---
共花费 0.07
最终运行三条查询的时间仅比一条(0.06)多一些,这就是异步夺门而入的速度了。
4 在jupyter中运行
jupyter很有意思,已经自动帮我们起了loop。所谓loop其实就是上面说的「经理」。所以在make_async_task_list
做名单的时候,出来的已经是执行完的结果。
因此在jupyter中不能用其他的loop语句,用gather感觉起来比较直观。
jupyter中提交名单的时候,直接就执行了(经理就在边上)
终端中则直到gather任务在去执行。
但是从代码的逻辑上,gather在两种模式下是等效的。(只是作用的时间顺序不同)
5 总结
总的说起来,异步就是对列表循环的加速。但是如果是计算密集的就免了,因为cpu已经没有能力再做其他事。
- 1 将需要执行的任务(通常是IO)进行参数化封装,称为任务类型
- 2 将具体要执行的任务进行参数化,封装到异步任务列表里
- 3 使用gather加速执行。