前言
在前面的文章,已经通过gevent实现高并发的协程,本文将详细讨论Python标准库异步IO——asyncio。在Python3.4中引入了协程的概念以及asyncio。asyncio底层调用yield from语法,将任务变成生成器后挂起,这种方式无法实现协程之间的自动切换,在Python3.5中正式确立引入了async和await 的语法,所有的这些工作都使得Python实现异步编程变得更容易上手。
1、asyncio的基本概念
-
event_loop 事件循环:每一个需要异步执行的任务都要注册到事件循环中,事件循环负责管理和调度这些任务之间的执行流程(遇到IO则自动切换协程等)。
-
coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
-
task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。
-
future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
-
async/await 关键字:在python3.5及以上,用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。
在异步的模式里,所有代码逻辑都会运行在一个forever事件循环中(你可以把整个事件循环看成一个总控中心,它监听着当前线程创建的多个协程发发生的事件),它可以同时执行多个协程,这些协程异步地执行,直到遇到 await 关键字,事件循环将会挂起该协程,事件循环这个总控再把当前线程控制权分配给其他协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。
2、使用asyncio
2.1 使用async关键字和await定义协程
在Python3.5之前,要实现协程方式的写法一般如下:
import asyncio
@asyncio.coroutine
async def mytask(task_id):
yield from asyncio.sleep(1)
在Python3.5以后,全面使用async关键字和await定义协程,代码显更直观。
import asyncio
import datetime
def get_time():
d=datetime.datetime.now()
return d.strftime('%H:%M:%S')
# async 定义了mytask为协程对象
async def mytask(task_id):
# 这里就像gevent的sleep方法模拟IO,而且该协程会被asyncio自动切换
print('task-{} started at:{}'.format(task_id,get_time()))
await asyncio.sleep(1) # await 要求该行语句的IO是有返回值的例如response=request.get(url),如果直接使用await time.sleep(2),则无法创建协程对象
print('task-{} done at:{}'.format(task_id,get_time()))
# 创建事件循环对象,该事件循环由当前主线程拥有
loop = asyncio.get_event_loop()
tasks=[mytask(i) for i in range(4)] # 这里mytask()是协程对象,不会离开运行。
loop.run_until_complete(asyncio.wait(tasks)) # 这里实行的逻辑就像gevent.joinall(tasks)一样,表示loop一直运行直到所有的协程tasks都完成
代码中通过async关键字定义一个协程(coroutine),不过该协程不能直接运行,需将它注册到事件循环loop里面,由后者在协程内部发生IO时(asyncio.sleep(2))时候调用协程。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。
输出,从结果可以看出,5个协程同一时刻并发运行。
task-1 started at:17:12:37
task-0 started at:17:12:37
task-3 started at:17:12:37
task-2 started at:17:12:37
task-4 started at:17:12:37
task-1 done at:17:12:38
task-0 done at:17:12:38
task-3 done at:17:12:38
task-2 done at:17:12:38
task-4 done at:17:12:38
关于await要求该句为返回值:
await asyncio.sleep(2),这里可以看看sleep返回什么
@coroutine
def sleep(delay, result=None, *, loop=None):
"""Coroutine that completes after a given time (in seconds)."""
if delay == 0:
yield
return result
if loop is None:
loop = events.get_event_loop()
future = loop.create_future()
h = future._loop.call_later(delay,
futures._set_result_unless_cancelled,
future, result)
try:
return (yield from future)
finally:
h.cancel()
如果设为delay值,且loop事件循环已创建(即使代码未创建它也会自动创建),返回的是future对象(yield from future),而这里可以挂起当前协程,直到future完成
2.2 task对象
...同上
# async 定义了mytask为协程对象
async def mytask(task_id):
# 这里就像gevent的sleep方法模拟IO,而且该协程会被asyncio自动切换
print('task-{} started at:{}'.format(task_id,get_time()))
await asyncio.sleep(1)
print('task-{} done at:{}'.format(task_id,get_time()))
return 'ok'
coro=mytask(1)
loop = asyncio.get_event_loop()
task=loop.create_task(coro) # 将协程对象封装为task对象
print('before register to loop:',task)
loop.run_until_complete(future=task)
print('after loop completed,task return the result:',task.result())
查看打印结果:
before register to loop: <Task pending coro=<mytask() running at /opt/asyn.py:9>>
task-1 started at:17:39:06
task-1 done at:17:39:07
after loop completed,task return the result: ok
将协程封装为task对象后,task在注册到事件循环之前为pending状态,1秒后,task 结束,并且通过task.result()可以获取协程结果值。
task对象也可用asyncio.ensure_future(coro)创建(接收coro协程或者future对象),它内部封装了loop.create_task
2.2 future对象
前面定义说了future表示将来执行或没有执行的任务的结果,task是future的子类。
基本的方法有:
• cancel(): 取消future的执行,调度回调函数
• result(): 返回future代表的结果
• exception(): 返回future中的Exception
• add_done_callback(fn): 添加一个回调函数,当future执行的时候会调用这个回调函数。
• set_result(result): 将future标为运行完成,并且设置return值,该方法常用
使用future,可以在协程结束后自行回调函数:
import asyncio
...同上
async def coru_1(future_obj,N):
print('coru_1 started at:{}'.format(get_time()))
total=sum(range(N))
await asyncio.sleep(2)
future_obj.set_result('coru_1 returns:{}'.format(total))
print('coru_1 done at:{}'.format(get_time()))
async def coru_2(future_obj,N):
print('coru_2 started at:{}'.format(get_time()))
total=sum(range(N))
await asyncio.sleep(2)
future_obj.set_result('coru_2 returns:{}'.format(total))
print('coru_2 done at:{}'.format(get_time()))
def call_back(future_obj):
time.sleep(1)
print('saved to redis at :',get_time(),future_obj,future_obj.result())
if __name__=='__main__':
loop=asyncio.get_event_loop()
f1=asyncio.Future()
f2=asyncio.Future()
tasks=[coru_1(f1,10),coru_2(f2,20)]
f1.add_done_callback(call_back)
f2.add_done_callback(call_back)
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
输出
coru_1 started at:16:52:07
coru_2 started at:16:52:07
coru_1 done at:16:52:09
coru_2 done at:16:52:09
saved to redis at : 16:52:10 <Future finished result='coru_1 returns:45'> coru_1 returns:45
saved to redis at : 16:52:11 <Future finished result='coru_2 returns:190'> coru_2 returns:190
两个协程同时启动且在同一时间结束运行。之后开始回调,可以看到协程1先回调,1秒完成后,再切换到协程2回调。
2.3 获取协程并发执行后的所有返回值
import asyncio
import datetime
def get_time():
d=datetime.datetime.now()
return d.strftime('%H:%M:%S')
async def read_file(task_id):
print('task-{} started at:{}'.format(task_id,get_time()))
await asyncio.sleep(2) # 模拟读取文件的耗时IO
return 'task-{} done at:{}'.format(task_id,get_time())
loop = asyncio.get_event_loop()
coros=[read_file(i) for i in range(5)] # 创建多个协程
tasks=[asyncio.ensure_future(coro) for coro in coros]# 将协程封装为task对象
loop.run_until_complete(asyncio.wait(tasks))
# 或者loop.run_until_complete(asyncio.gether(*tasks))
# 重点在这里,当所有的协程结束后,可批量获取所有协程的返回结果
get_all_result=[ t.result() for t in tasks]
print(get_all_result)
输出:
task-0 started at:15:53:08
task-1 started at:15:53:08
task-2 started at:15:53:08
task-3 started at:15:53:08
['task-0 done at:15:53:09', 'task-1 done at:15:53:09', 'task-2 done at:15:53:09', 'task-3 done at:15:53:09']
以上也无需使用future的回调机制获取协程返回值,直接在loop结束后,从task对象的result方法即可获得协程返回值。
需要注意的是:
用于等待所有协程完成的方法asyncio.wait和asyncio.gather,都是接受多个future或coro组成的列表,区别:asyncio.gather内边调用ensure_future方法将列表中不是task的coro封装为future对象,而wait则没有。
2.4 asyncio.gather vs asyncio.wait
这里再给两个例子说明这两者的区别以及应用场合:
asyncio.gather
import asyncio
import datetime
def get_time():
d=datetime.datetime.now()
return d.strftime('%M:%S')
async def coro(group_id,coro_id):
print('group{}-task{} started at:{}'.format(group_id,coro_id,get_time()))
await asyncio