为何asyncio.sleep会影响任务取消时机及执行状态?
import asyncio async def cancel_me(): print('cancel_me(): before sleep') try: # 等待1小时 await asyncio.sleep(3600) except asyncio.CancelledError: print('cancel_me(): cancel sleep') raise finally: print('cancel_me(): after sleep') async def main(): # 创建一个"cancel_me"任务 task = asyncio.create_task(cancel_me()) # 如果注释掉这行,会直接输出 -> main(): cancel_me is cancelled now await asyncio.sleep(1) # 这会安排在下一轮事件循环时,给被包装的协程抛出CancelledError异常 task.cancel() try: await task except asyncio.CancelledError: print("main(): cancel_me is cancelled now") asyncio.run(main())
不同场景下的输出
保留await asyncio.sleep(1)时的输出
cancel_me(): before sleep cancel_me(): cancel sleep cancel_me(): after sleep main(): cancel_me is cancelled now
注释掉await asyncio.sleep(1)时的输出
main(): cancel_me is cancelled now
问题解答
1. 为何仅注释该asyncio.sleep命令会产生上述输出差异?
asyncio.create_task只是把任务调度到事件循环的待执行队列,不会立即执行。如果没有await asyncio.sleep(1),main协程会直接走到task.cancel()——此时cancel_me还没开始执行,取消一个未启动的任务不会触发协程内部的任何逻辑,后续await task直接抛出CancelledError,只会输出main()里的提示。
保留await asyncio.sleep(1)时,main协程挂起1秒,事件循环会趁机执行cancel_me:它先打印before sleep,然后挂起在await asyncio.sleep(3600)上。1秒后main恢复执行并调用task.cancel(),此时CancelledError会被抛进正在挂起的cancel_me协程,触发异常捕获和finally逻辑,所以会输出完整的日志。
2. 是否create_task后的任何await都会启动已创建/调度的任务?
是的。create_task只是把任务加入事件循环队列,事件循环只有在当前协程挂起(遇到await操作)时,才会切换去执行队列里的其他任务。所以只要main里有任何await(不管是sleep、await task还是其他异步操作),已调度的cancel_me任务都会被启动执行。
3. 在将main()中await asyncio.sleep(1)之后的代码注释,替换为await asyncio.sleep(5)且未调用task.cancel()时,cancel_me()中的CancelledError异常仍被触发并打印cancel_me(): cancel sleep,这是为何?
当main执行到await asyncio.sleep(5)时,cancel_me会先启动执行,然后挂起在await asyncio.sleep(3600)上。5秒后main协程执行完毕,asyncio.run()会关闭事件循环,此时所有未完成的任务都会被自动取消——事件循环会给这些任务抛出CancelledError,因此cancel_me里的异常捕获逻辑被触发,打印对应内容后执行finally块。
内容的提问来源于stack exchange,提问作者An old man in the sea.

