python协程asyncio的个人理解
协程与任务
python语境中,协程 coroutine 的概念有两个:协程函数、协程对象,协程对象由协程函数创建得到(类似于类实例化得到一个对象).
理解协程,最重要的是了解事件循环和任务执行的机制,下面是三个原则:
- 事件循环中,不断循环执行各个任务,若一个任务遇到await或者执行完成,则返回控制权给事件循环,这时候事件循环再去执行下一个任务
- 事件循环同一时刻只会运行一个任务
- 协程不会被加入事件循环的执行日程,只有被注册为任务之后,事件循环才可以通过任务来设置日程以便并发执行协程
基本语法
协程的声明和运行
使用async def
语句定义一个协程函数,但这个函数不可直接运行
async def aaa():
print('hello')
print(aaa())
# 输出----------------------------------
<coroutine object aaa at 0x7f4f9a9dfec0>
/root/Project/test01/test2.py:4: RuntimeWarning: coroutine 'aaa' was never awaited
print(aaa())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
如何运行一个协程呢,有三种方式:
- 使用
asyncio.run()
函数,可直接运行
import asyncio
async def aaa():
print('hello')
asyncio.run(aaa())
# 输出-------------------------
hello
- 使用
await
进行异步等待
在协程函数中最重要的功能是使用await
语法等待另一个协程,这将挂起当前协程,直到另一个协程返回结果。
await
的作用:挂起 coroutine 的执行以等待一个 awaitable 对象。 只能在 coroutine function 内部使用。
import asyncio
async def aaa():
print('hello')
async def main():
await aaa()
asyncio.run(main())
- 使用
asyncio.create_task()
函数来创建一个任务,放入事件循环中
import asyncio
async def aaa():
print('hello')
async def main():
asyncio.create_task(aaa())
asyncio.run(main())
可等待对象
上面说过,协程函数中最重要的功能是使用await
语法等待另一个协程,这将挂起当前协程,直到另一个协程返回结果。(重要,重复一遍)
await后面需要跟一个可等待对象(awaitable),有下面三种可等待对象:
- 协程:包括协程函数和协程对象
- 任务:通过asyncio.create_task()函数将协程打包为一个任务
- Futures:特殊的 低层级 可等待对象,表示一个异步操作的 最终结果
运行asyncio程序
asyncio.run
(coro, ***, debug=False)
传入协程coroutine coro ,创建事件循环,运行协程返回结果,并在结束时关闭,应当被用作 asyncio 程序的主入口点。
创建任务
asyncio.create_task
(coro, ***, name=None)
将 coro 协程 打包为一个 Task 排入日程准备执行。返回 Task 对象。
休眠
coroutine asyncio.sleep
(delay, result=None, ***, loop=None)
阻塞 delay 指定的秒数,该协程总是会挂起当前任务,以允许其他任务运行
机制解析
通过官网的两段代码,来详细解析一下协程的运行机制。
官方两个代码如下,注意看输出差异:
代码1,通过协程对象来执行
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
asyncio.run(main()) # 1: 创建事件循环,传入入口点main()协程对象,此时生成一个对应的task
输出为:
started at 17:13:52
hello
world
finished at 17:13:55
代码2,通过任务来执行
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
task1 = asyncio.create_task(
say_after(1, 'hello'))
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
await task1
await task2
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
输出:
started at 17:14:32
hello
world
finished at 17:14:34
注意到运行时间比前一个代码快1秒,下面说明为什么出现这种情况(文字比较多)。
代码一的运行逻辑:
asyncio.run(main()) 启动一个事件循环,将入口点main()协程对象传入,生成一个对应的任务task_main;
事件循环运行任务task_main,然后执行第1条代码:print(f"started at {time.strftime('%X')}");
接着执行第2条代码:await say_after(1, 'hello'),第2条代码首先生成一个say_after(1, 'hello')协程对象,同时生成该协程对象对应的task_1;
由于await语法,task_main任务将控制权返回给事件循环,同时告诉事件循环需要等待task1才能继续运行;
事件循环获得控制权后,发现此时有两个任务task_main和task1,同时task_main在等待task1,于是会去执行task1任务;
task1任务将执行第1条代码:await asyncio.sleep(1),同样会生成asyncio.sleep(1)协程对象,以及对应的任务task2,同时因await语法将控制权返回给事件循环;
事件循环获得控制权后,发现此时有三个任务task_main、task1、task2,由于task_main、task1都处于等待状态,于是执行task3;
task3在1秒后运行完成,返回控制权给事件循环;
事件循环获得控制权,发现此时有两个任务task_main和task1,同时task_main在等待task1,于是会去执行task1任务;
task1任务执行第2条代码:print('hello'),执行完成后,任务也运行结束,将控制权返回给事件循环;
事件循环获得控制权后,发现此时有一个任务task_main,于是接着执行下一条代码:await say_after(2, 'world'),继续重复上述过程,直到这个协程任务结束;
task_main执行最后一条代码;
事件循环关闭退出;
代码二的运行逻辑:
asyncio.run(main()) 启动一个事件循环,将入口点main()协程对象传入,生成一个对应的任务task_main;
事件循环运行任务task_main,然后执行前几条代码,创建两个任务task1、task2,并注册到事件循环中(此时事件循环一共有3个task),随之执行程序直到await;
第一个await:await task1,这里会阻塞当前任务task_main并将控制权返回给事件循环,事件循环获取控制权,安排执行下一个任务task1;
task1任务开始执行,直至遇到await asyncio.sleep(1),asyncio.sleep(1)协程对象开始异步执行,同时task1返回控制权给事件循环,事件循环获取控制权后安排执行下一个任务task2;
task2任务开始执行,直至遇到await asyncio.sleep(2),asyncio.sleep(2)协程对象开始异步执行,同时task2返回控制权给事件循环,事件循环获取控制权后安排执行下一个任务;
此时3个任务均处于await状态,事件循环保持等待;
1秒后asyncio.sleep(1)执行完成,task1取消阻塞,事件循环将安排task1执行,task1执行完成后返回控制权给事件循环,此时事件循环中一共两个任务task_main、task2。
此时task2任务处于await状态,而task_main也取消了阻塞,事件循环安排task_main执行,执行一行代码后遇到await task2,于是返回控制权给事件循环;
此时2个任务均处于await状态,事件循环保持等待;
1秒后asyncio.sleep(2)执行完成,task2取消阻塞,事件循环将安排task2执行,task2执行完成后返回控制权给事件循环,此时事件循环中只剩任务task_main;
于是事件循环安排task_main执行,task_main执行完成,asyncio.()函数收到信息也结束运行,整个程序结束
运行的流程图示
(任务就绪后,就等待事件循环来调用了,此时需要await来阻塞主任务task_main,否则控制权一直在task_main手上,导致task_main任务执行完成,run()收到main()执行结束的消息后,事件循环也关闭并结束,程序也将退出)
其实将第2个代码中的await task1删除,只保留await task2,结果中的输出相同,并消耗相同的总时间。但只保留await task1的话,将没有task2的输出;
如果将第2个代码中的await task1和await task2都删除,换成await asyncio.sleep(3),一样会打印相同输出,不过总时间会变为3秒;
其中的原因需要理解协程的工作机制(事件循环和控制权)