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

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+ 中CancelledError是Exception子类,所以真会被这个 except 捕获) - 正确做法:要么显式写
except (CancelledError, ...):并重新 raise,要么用except BaseException:+ 判断类型再决定是否处理 - 使用
async with或finally做清理时,记得在其中也检查task.cancelled(),否则资源可能泄漏
嵌套任务取消的传播边界
父任务取消时,默认不会自动取消它 await 的子任务。asyncio 不做隐式级联取消,这是设计选择,不是 bug。
立即学习“Python免费学习笔记(深入)”;
- 典型场景:主任务
await subtask,此时调用main_task.cancel(),subtask仍活着 - 若需联动取消,得手动触发:
subtask.cancel()放在父任务的finally或except CancelledError块里 - 用
asyncio.gather(..., return_exceptions=True)时,单个子任务被取消不会影响其他,但整个gather任务的状态取决于你是否await它——它本身可被独立取消 - 注意
asyncio.create_task()创建的任务脱离当前作用域后,没人引用就可能被 GC 掉,但取消状态不受影响;而asyncio.ensure_future()行为一致,无需替换
真正难的不是调用 cancel(),是让每个 await 都成为潜在的退出点,同时确保异常不被意外吃掉。很多人卡在“以为取消了”,其实是任务卡死在某个没 await 的循环里,或者被一层宽泛的 except Exception 拦住了。