说明

异步调用真的挺容易忘的,而且忘的很彻底…
安利下自己的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运行是没用的(看起来像异步的同步)。

python request库 异步 python 异步接口_异步任务


事实上,一个异步函数不能自己「做主」,而是要向「经理」报到,表示自己是个异步函数,服从安排。然后再由经理(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个)

python request库 异步 python 异步接口_同步函数_02


异步方式需要让这些任务向「经理」报到,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做名单的时候,出来的已经是执行完的结果。

python request库 异步 python 异步接口_同步函数_03


因此在jupyter中不能用其他的loop语句,用gather感觉起来比较直观。

jupyter中提交名单的时候,直接就执行了(经理就在边上)
终端中则直到gather任务在去执行。

但是从代码的逻辑上,gather在两种模式下是等效的。(只是作用的时间顺序不同)

5 总结

总的说起来,异步就是对列表循环的加速。但是如果是计算密集的就免了,因为cpu已经没有能力再做其他事。

  • 1 将需要执行的任务(通常是IO)进行参数化封装,称为任务类型
  • 2 将具体要执行的任务进行参数化,封装到异步任务列表里
  • 3 使用gather加速执行。