事件循环的内部运作机制对我们来说基本是黑盒,它自行决定何时以及如何执行协程和任务。不过,如果真有需要,我们也可以手动触发一次事件循环的迭代。这在某些场景下很有用,比如让一个耗时很长的任务尽早开始,避免阻塞整个事件循环(但这种情况下也应考虑使用线程),或者确保某个任务能够立刻启动。
回想一下,当我们创建多个任务时,它们都不会立即开始执行,直到遇到一个 await 点。这个 await 会触发事件循环去调度和启动这些任务。但如果我们希望每一个任务都立刻就开始运行,该怎么办呢?
asyncio 提供了一个优化的惯用法:向 asyncio.sleep(0) 传入零,来挂起当前协程并强制进行一次事件循环迭代。我们来看看怎么用这个技巧来尽快启动任务。我们要写两个函数,一个不用 sleep,一个用,来对比它们的执行顺序。
import asynciofrom util import delayasync def create_tasks_no_sleep(): task1 = asyncio.create_task(delay(1)) task2 = asyncio.create_task(delay(2)) print('Gathering tasks:') await asyncio.gather(task1, task2)async def create_tasks_sleep(): task1 = asyncio.create_task(delay(1)) await asyncio.sleep(0) task2 = asyncio.create_task(delay(2)) await asyncio.sleep(0) print('Gathering tasks:') await asyncio.gather(task1, task2)async def main(): print('--- Testing without asyncio.sleep(0) ---') await create_tasks_no_sleep() print('--- Testing with asyncio.sleep(0) ---') await create_tasks_sleep()asyncio.run(main())
--- Testing without asyncio.sleep(0) ---Gathering tasks:sleeping for 1 second(s)sleeping for 2 second(s)finished sleeping for 1 second(s)finished sleeping for 2 second(s)--- Testing with asyncio.sleep(0) ---sleeping for 1 second(s)sleeping for 2 second(s)Gathering tasks:finished sleeping for 1 second(s)finished sleeping for 2 second(s)
首先,我们创建了两个任务,然后直接 gather 它们,没有用 asyncio.sleep(0)。这是预期的结果,两个 delay 协程直到 gather 语句才开始执行。接着,我们在每次创建任务后都插入了一个 asyncio.sleep(0)。在输出中,你会注意到 delay 协程的消息会在调用 gather 之前就立即打印出来。这是因为 sleep(0) 触发了一次事件循环迭代,导致我们任务里的代码立刻就执行了。
我们现在主要用的是 asyncio 自带的事件循环实现。但其实还有别的实现方式可供替换。接下来,我们来看看如何使用不同的事件循环。