Python asyncio 中任务取消的实现机制

1次阅读

asyncio.cancel() 不生效的根本原因是取消为协作式,仅设标记,任务须在await点检查并抛出cancellederror;若无await或cancellederror被except exception吞掉,则取消失败。

Python asyncio 中任务取消的实现机制

asyncio.cancel() 为什么有时不生效

根本原因不是函数没调用,而是被取消的任务仍在运行中、没遇到 await 点。asyncio 的取消是协作式的——它只设置任务的取消标记,真要退出得靠任务自己在下一次 await 时检查并抛出 CancelledError

  • 常见错误现象:task.cancel()task.done() 仍是 False,甚至任务继续打印日志或发请求
  • 典型场景:任务里写了个长循环(如 while True:)但没 await asyncio.sleep(0) 或其他可取消的挂起点
  • 必须确保所有可能阻塞的路径上都有可中断的 await,比如用 await asyncio.wait_for(..., timeout=...) 替代无超时的 await
  • 如果任务正在执行 CPU 密集型逻辑(如大列表推导),cancel() 完全无效——asyncio 不支持抢占式中断

如何安全地等待任务响应取消

直接 await task 可能永远卡住;用 asyncio.wait_for(task, timeout=...) 也不够,因为超时后任务还在后台跑。正确做法是用 asyncio.shield() + 显式捕获异常,或者更推荐:用 asyncio.create_task() 后配合 asyncio.wait() 等待完成或取消。

  • 别写:await task —— 一旦任务没响应取消,协程就彻底挂起
  • 推荐写法:done, pending = await asyncio.wait({task}, return_when=asyncio.FIRST_COMPLETED),然后检查 task.done()task.cancelled()
  • 若需带超时且确保清理,应把 task.cancel()await asyncio.wait_for(task, timeout=...) 包进 try/except CancelledError
  • asyncio.shield() 会阻止取消传播,仅用于临时保护关键子任务,别误用在主任务上

CancelledError 被吞掉的典型位置

只要没在 except 块里显式处理 CancelledError,它就可能被意外吞掉,导致取消失败却无提示。

  • 常见错误现象:任务被取消,但没抛异常、也没退出,日志里完全静默
  • 高频出问题的地方:except Exception: —— 这会捕获 CancelledError(它是 BaseException子类,但不在 Exception 继承链上?错!python 3.8+ 中 CancelledErrorException 子类,所以真会被这个 except 捕获)
  • 正确做法:要么显式写 except (CancelledError, ...): 并重新 raise,要么用 except BaseException: + 判断类型再决定是否处理
  • 使用 async withfinally 做清理时,记得在其中也检查 task.cancelled(),否则资源可能泄漏

嵌套任务取消的传播边界

父任务取消时,默认不会自动取消它 await 的子任务。asyncio 不做隐式级联取消,这是设计选择,不是 bug

立即学习Python免费学习笔记(深入)”;

  • 典型场景:主任务 await subtask,此时调用 main_task.cancel()subtask 仍活着
  • 若需联动取消,得手动触发:subtask.cancel() 放在父任务的 finallyexcept CancelledError 块里
  • asyncio.gather(..., return_exceptions=True) 时,单个子任务被取消不会影响其他,但整个 gather 任务的状态取决于你是否 await 它——它本身可被独立取消
  • 注意 asyncio.create_task() 创建的任务脱离当前作用域后,没人引用就可能被 GC 掉,但取消状态不受影响;而 asyncio.ensure_future() 行为一致,无需替换

真正难的不是调用 cancel(),是让每个 await 都成为潜在的退出点,同时确保异常不被意外吃掉。很多人卡在“以为取消了”,其实是任务卡死在某个没 await 的循环里,或者被一层宽泛的 except Exception 拦住了。

text=ZqhQzanResources