关于Python asyncio任务执行顺序与异常处理的疑问
问题分析:asyncio中临界睡眠时间下的任务执行顺序
先明确核心现象:当main中的await asyncio.sleep(1.0000000001)时,任务的后续代码在main结束后执行,且异常未被捕获,这是由事件循环的定时器调度精度和任务执行时机共同决定的。
基础机制回顾
asyncio.create_task()会将协程包装为Task并加入事件循环的就绪队列,当前任务(如main)遇到await时,事件循环会切换到其他就绪Task执行。asyncio.sleep()会创建一个定时器,到期后将对应的协程重新加入就绪队列,等待事件循环调度。
三种场景的执行顺序拆解
场景1:await asyncio.sleep(1.1)
main启动,打印main started,创建task并加入就绪队列。main执行await sleep(1.1)挂起,事件循环调度task执行:打印start executing task,然后await sleep(1.0)挂起。- 1.0秒后,
task的定时器到期,事件循环唤醒task:打印done waiting in task,抛出异常,task进入**完成(带异常)**状态。 - 再过0.1秒,
main的定时器到期,main恢复执行:打印done waiting in main,调用task.result()捕获异常,最后打印end of main正常结束。
场景2:await asyncio.sleep(0.1)
main启动,创建task后挂起等待0.1秒,事件循环调度task执行到await sleep(1.0)挂起。- 0.1秒后
main恢复执行,此时task仍在等待1.0秒的定时器,处于PENDING状态,调用task.result()抛出Result is not set.。 main结束后,asyncio.run()终止事件循环,task被直接取消,不会执行后续代码。
场景3:await asyncio.sleep(1.0000000001)(核心疑问场景)
这里的关键是两个定时器的时间差极小,超出了系统时钟的调度精度,导致事件循环的执行顺序出现如下情况:
main启动,创建task后挂起等待1.0000000001秒,事件循环调度task执行到await sleep(1.0)挂起。- 理论上
task的1.0秒定时器先到期,但由于系统时钟的粒度限制(如操作系统时钟精度为毫秒级),事件循环先检测到main的1.0000000001秒定时器到期,唤醒main继续执行。 main恢复后打印done waiting in main,此时task的定时器虽然已经到期,但事件循环还没来得及调度task恢复执行,task仍处于PENDING状态,因此task.result()抛出Result is not set.。main打印end of main后执行完毕,asyncio.run()准备关闭事件循环,但此时事件循环发现task已经处于就绪状态(定时器到期),于是调度task执行剩余代码:打印done waiting in task,然后抛出异常。- 由于
main已经执行完毕,没有任何代码会捕获task的异常,因此事件循环输出Task exception was never retrieved的错误信息。
总结
这种现象是异步编程中临界时间调度的典型案例,本质是系统时钟精度无法区分极小的时间差,导致事件循环的任务调度顺序与预期出现偏差。如果需要确保任务的异常被捕获,建议使用await task替代task.result(),因为await会等待任务完成,无论其执行时机如何,都能正确捕获异常。
内容的提问来源于stack exchange,提问作者charlielao
相关产品推荐
相关产品推荐

