asyncio中事件循环如何调度任务?

2 投票
1 回答
50 浏览
提问于 2025-04-14 15:26

关于Asyncio程序输出不如预期

大家好。我最近在研究asyncio,想了解事件循环、任务、协程等是怎么工作的。从这个这个讨论中,我了解到任务在遇到await关键字时并不会把控制权交还给事件循环,而是在遇到yield时才会交还。然后事件循环会去寻找其他已经提交的任务并执行它们。说了这么多,我有一个测试程序如下:

import asyncio
from datetime import datetime

async def a():
    print(f"start a {datetime.now()}")
    await asyncio.sleep(0)
    print(f"done a {datetime.now()}")

async def b():
    print(f"start b {datetime.now()}")
    await asyncio.sleep(0)
    print(f"done b {datetime.now()}")

async def main():
    print(f"time start main: {datetime.now()}")
    a_result = asyncio.create_task(a())
    b_result = asyncio.create_task(b())

    # await a_result
    # await b_result

    print(asyncio.all_tasks())

    print(f"time done main: {datetime.now()}")

if __name__ == "__main__":
    asyncio.run(main())
    print(f"back to main: {datetime.now()}")

输出结果是:

time start main: 2024-03-17 11:24:53.119904
{<Task pending name='Task-1' coro=<main() running at /home/nam/practice/main.py:24> cb=[_run_until_complete_cb() at /usr/lib/python3.10/asyncio/base_events.py:184]>, <Task pending name='Task-2' coro=<a() running at /home/nam/practice/main.py:4>>, <Task pending name='Task-3' coro=<b() running at /home/nam/practice/main.py:10>>}
time done main: 2024-03-17 11:24:53.120038
start a 2024-03-17 11:24:53.120076
start b 2024-03-17 11:24:53.120110
back to main: 2024-03-17 11:24:53.120266

我原本期待程序的执行顺序是这样的:

  1. 进入asyncio.run()的入口
  2. 打印出主函数的开始时间
  3. 创建(但不执行)包装协程ab的任务
  4. 打印出任务列表
  5. 打印出主函数的结束时间
  6. 打印“回到主函数”的语句
  7. 结束

但是从输出结果来看,实际情况并不是这样。任务a和任务b还是成功运行了。我的问题是,当main协程中没有await点时,这两个任务是怎么有机会运行的?还是说这是asyncio.run函数造成的某种魔法?

1 个回答

1

“这是不是因为asyncio.run这个函数的魔力呢?”

从我简单浏览这个库的源代码来看,答案很简单,就是“是的”。这个run函数会创建一个任务,然后调用loop.run_until_complete(task),这一步会为这个任务创建一个未来对象(future),接着再调用loop.run_forever()。正如你观察到的,这个方法至少会执行一次完整的循环。

在这次循环结束时,循环会退出,因为和task相关的未来对象已经完成了。而你创建的另外两个任务则没有完成。

asyncio的一个主要概念是,任务之间是相互独立的,除非你特别去同步它们。

撰写回答