asyncio.run()中任务取消后cleanup不执行,因未捕获CancelledError导致事件循环直接退出;正确做法是在协程内用except asyncio.CancelledError处理清理,而非依赖finally。

asyncio.run() 里任务被取消后,为什么 cleanup 代码不执行?
因为 asyncio.run() 遇到未处理的 CancelledError 会直接退出事件循环,协程里 finally 或 async with 的清理逻辑可能根本没机会运行——尤其当取消发生在 await 点上、且你没显式捕获时。
正确做法是:在可取消的协程内部主动捕获 CancelledError,把清理逻辑放在 except asyncio.CancelledError: 块里,而不是只依赖 finally。
-
finally在协程被取消时不一定执行(取决于取消时机和 Python 版本),但except asyncio.CancelledError一定触发 - 若用
async with获取资源,确保其__aexit__方法本身不抛异常,否则清理中断 - 避免在
except asyncio.CancelledError里再 await 其他协程——除非加shield,否则可能再次被取消
task.cancel() 后 await task 为什么会卡住或报 RuntimeError?
调用 task.cancel() 只是“发起取消请求”,并不会立即终止协程;必须 await task 才能真正传播 CancelledError 并完成清理。但如果协程里没处理该异常,或者在取消后又试图 await 已经被取消的 task,就会触发 RuntimeError: Task is cancelled。
- 永远不要在
task.cancel()后直接await task而不加try/except - 推荐写法:
try: await task except asyncio.CancelledError: pass—— 显式吸收取消信号 - 若需等待清理完成,可在
except asyncio.CancelledError里做同步清理,或启动一个新 task 处理异步收尾(用asyncio.create_task()+asyncio.shield())
如何让长耗时的 sync 操作(如 time.sleep、requests.get)响应取消?
纯同步阻塞调用(比如 time.sleep(10)、requests.get())无法被 asyncio 取消——事件循环被卡住,取消信号进不来。这不是 bug,是设计限制。
立即学习“Python免费学习笔记(深入)”;
- 替换
time.sleep()→await asyncio.sleep() - HTTP 请求必须用异步客户端:如
aiohttp或httpx.AsyncClient,而非requests - 对无法修改的 sync 函数,可用
loop.run_in_executor()包装,但注意:线程内取消不可靠,仅能靠超时或标志位协作退出 - 若必须用
requests,至少加上timeout=参数,避免无限 hang
asyncio.shield() 和 asyncio.wait_for() 搭配取消时容易出什么问题?
asyncio.shield() 会让被包裹的协程“免疫”外部取消,但如果你在它外面套了 asyncio.wait_for(),而超时触发取消,结果就是:wait_for 报 TimeoutError,但 shield 内部的协程还在跑,且后续没人管它——资源泄漏、状态错乱都可能发生。
- 不要对需要清理的长期任务盲目用
shield();它适合保护“关键子步骤”,不是兜底方案 - 若用
wait_for(),清理逻辑必须放在外层except TimeoutError或finally中,不能指望 shield 内部自己处理 - 更稳妥的做法:用
asyncio.create_task()启动任务,再用asyncio.wait()+return_when=asyncio.FIRST_COMPLETED控制竞争,手动 cancel 未完成者
实际项目里最常被忽略的点是:取消不是“停止”,而是“通知协程自行退出”。 协程是否响应、何时响应、清理是否完整,全由你写的 except asyncio.CancelledError 和资源管理逻辑决定。没有自动兜底。



















