Python Asyncio通过Future和Task的封装来实现协程的调度,而在Python Asyncio之中Coroutines, Tasks和Future都属于可等待对象,在使用的Asyncio的过程中,经常涉及到三者的转换和调度,开发者容易在概念和作用上犯迷糊,本文主要阐述的是三者之间的关系以及他们的作用。
1.Asyncio的入口
协程是线程中的一种特例,协程的入口和切换都是靠事件循环来调度的,在新版的Python
中协程的入口是Asyncio.run
,当程序运行到Asyncio.run
后,可以简单的理解为程序由线程模式切换为协程模式(只是方便理解,对于计算机而言,并没有这样区分),以下是一个最小的协程例子代码:
import asyncio
async def main():
await asyncio.sleep(0)
asyncio.run(main())
在这段代码中,main
函数和asyncio.sleep
都属于Coroutine,main
是通过asyncio.run
进行调用的,接下来程序也进入一个协程模式,asyncio.run
的核心调用是Runner.run
,它的代码如下:
class Runner:
...
def run(self, coro, *, context=None):
"""Run a coroutine inside the embedded event loop."""
# 省略代码
...
# 把coroutine转为task
task = self._loop.create_task(coro, context=context)
# 省略代码
...
try:
# 如果传入的是Future或者coroutine,也会专为task
return self._loop.run_until_complete(task)
except exceptions.CancelledError:
# 省略代码
...
这段代码中删去了部分其它功能和初始化的代码,可以看到这段函数的主要功能是通过loop.create_task
方法把一个Coroutine对象转为一个Task对象,然后通过loop.run_until_complete
等待这个Task运行结束。
在
Python3.5
之后,asycnio
改为又C语言实现,所以本文是asyncio
源码都来源于最后一个以Python
实现的asycnio版本Python3.4.10/Lib/asyncio[2]
可以看到,Asycnio
并不会直接去调度Coroutine,而是把它转为Task再进行调度,这是因为在Asyncio
中事件循环的最小调度对象就是Task。不过在Asyncio
中并不是所有的Coroutine的调用都会先被转为Task对象再等待,比如示例代码中的asyncio.sleep
,由于它是在main
函数中直接await的,所以它不会被进行转换,而是直接等待,在这个图示中,从main
函数到asyncio.sleep
函数中没有明显的loop.create_task
等把Coroutine转为Task调用,这里之所以不用进行转换的原因不是做了一些特殊优化,而是本因如此, 这个await asyncio.sleep
函数实际上还是会被main
这个Coroutine转换成的Task
继续调度到。
2.两种Coroutine调用方法的区别
在了解Task
的调度原理之前,还是先回到最初的调用示例,看看直接用Task调用和直接用Coroutine调用的区别是什么。如下代码,我们显示的执行一个Coroutine转为Task的操作再等待,那么代码会变成下面这样:
import asyncio
async def main():
await asyncio.create_task(asyncio.sleep(0))
asyncio.run(main())
这样的代码看起来跟最初的调用示例很像,没啥区别,但是如果进行一些改变,比如增加一些休眠时间和Coroutine的调用,就能看出Task对象的作用了,现在编写两份文件,他们的代码如下:
# demo_coro.py
import asyncio
import time
async def main():
await asyncio.sleep(1)
await asyncio.sleep(2)
s_t = time.time()
asyncio.run(main())
print(time.time() - s_t)
# // Output: 3.0028765201568604
# demo_task.py
import asyncio
import time
async def main():
task_1 =