事出有因
先来看两段关于asyncio的代码:
代码段一:
import asyncio
async def worker_1():
print('worker_1 start')
await asyncio.sleep(1)
print('worker_1 done')
async def worker_2():
print('worker_2 start')
await asyncio.sleep(2)
print('worker_2 done')
async def main():
print('before await')
await worker_1()
print('awaited worker_1')
await worker_2()
print('awaited worker_2')
%time asyncio.run(main())
########## 输出 ##########
before await
worker_1 start
worker_1 done
awaited worker_1
worker_2 start
worker_2 done
awaited worker_2
Wall time: 3 s
代码段二:
import asyncio
async def worker_1():
print('worker_1 start')
await asyncio.sleep(1)
print('worker_1 done')
async def worker_2():
print('worker_2 start')
await asyncio.sleep(2)
print('worker_2 done')
async def main():
task1 = asyncio.create_task(worker_1())
task2 = asyncio.create_task(worker_2())
print('before await')
# 下面的await task1/task2 换成 await asyncio.sleep(1) 也是可以的,道理一样,下面会讲
await task1
print('awaited worker_1')
await task2
print('awaited worker_2')
%time asyncio.run(main())
########## 输出 ##########
before await
worker_1 start
worker_2 start
worker_1 done
awaited worker_1
worker_2 done
awaited worker_2
Wall time: 2.01 s
问题
代码段一里面的协程(coroutine)换成代码段二的任务(task)后,为什么执行顺序就变了?这个过程中发生了什么事情?
关于asyncio的执行过程
- 先直接看代码二,整个函数的运行从asyncio.run(main())这里开始的。
- 跳进去
asyncio.run()
这个函数可以看到它首先创建了一个event loop(事件循环),这是整个asyncio的基础,没有事件循环就没有办法很好地调度各个协程。 - 然后调用了
loop.run_until_complete()
并将mian这个主协程传递进去函数。 run_until_complete()
位于base_events.py,函数有句注释:
Run until the Future is done.If the argument is a coroutine, it is wrapped in a Task.
所以函数会调用tasks.ensure_future()
来将一个coroutine转化为task,经过一些处理(绑定协程完成时的回调函数)之后就进入到事件循环的核心函数run_forever()
- 在
run_forever()
函数中比较关键的一句代码是self._run_once()
。跳进去这个_run_once()函数会发现它会从self._ready
这个放着准备要执行的task/future队列中拿出一个task(self._ready.popleft()
),通过handle._run()
来执行该task。
- 跳进去
上面的过程是ayncio的事件循环如何启动一个协程执行的过程。
-
再来看代码段二中main()函数的主要内容
- 进到main()后直接调用了
asyncio.create_task()
将worker_1和worker_2两个coroutine转化成了task对象。 - 跳进去
asyncio.create_task()
函数查看(最后trace back到base_events.py中的create_task()),关键的一句代码是task = tasks.Task(coro, loop=self)
,这里将一个协程包装成了一个任务。 - 去查看tasks.Task 这个类的初始化过程,里面关键的一句代码是:
self._loop.call_soon(self.__step, context=self._context)
。可以看到self.__step
这个函数被作为参数传递了进去。查看self.__step
这个函数可以发现,它会获取当前要执行的协程并通过result = coro.send(None)
来触发协程执行。 - 看回
self._loop.call_soon()
函数,它里面又调用了_call_soon()
函数将__step函数帮装成了event.Handle()对象,并将这个对象放进了self._ready这个 用来存放即将可以执行的Handle对象的队列 里。也即加入到了下一轮时间循环中。 - 至此,coroutine通过调用
asyncio.create_task()
而转化成task,进而会被加入到事件循环中。
- 进到main()后直接调用了
-
然后大概说一下代码二中main()函数的执行
- 调用
asyncio.run(main())
后 main() 这个coroutine会被加入到事件循环并被执行。然后进到main()函数中,worker_1()、worker_2()通过asyncio.create_task()
由coroutine转化为task1、task2并被加入到事件循环中。 - main()这个协程在执行到
await task1
后被挂起,让出控制权,由事件循环调度决定执行去执行task1还是task2。 作为事件循环,当 await task1 把控制权交还的时候,这一轮循环已经结束,随后立即开始下一轮的事件循环,在下一轮循环中检查有没有其他任务(task1和task2) - 其实可以用一个比较简单的方式解释,await 导致了 task 切换,但是 create_task 之后这个 task 已经放进等待列表,所以在 main task 中 await 任何 task 都会导致 worker 被执行,至于 worker1 和 worker2 的执行顺序,就交由事件循环去调度了。这也是在解释代码二里的注释为什么
await task1
也可以换成await asyncio.sleep(1)
。
- 调用
-
至于代码段一
- 代码段一中的main()函数没有将worker_1()、worker_2()转化成task,也即没有先加入到事件循环中。
- 代码执行到
await worker_1()
的时候才会把worker_1()这个协程加入到事件循环中并且执行,在执行过程中遇到await asyncio.sleep(1)
,然而此时循环中也没有别的可执行任务,于是就干等了。await worker_2()
的情况类似。 - 所以代码一的写法跟直接用同步的方式写没有什么区别。它只是用异步的写法写了一个同步执行的代码。
还有一点点
行文至此想说的能说的基本上都差不多了,如果有什么不对的地方还请多多指正。
另外在探究这个问题的时候提了一些问题以及得到了不少v友的提点,大家如果去看看下面两个链接的话可能会更有帮助,是我的两个提问帖子。
帖子一传送门(python3.7 中的 async/await 以及 asyncio 问题)
帖子二传送门(不死心,再来问一遍关于 Python 的 asyncio 问题)