嵌套异步任务取消不会自动穿透子协程,需显式传递信号或使用TaskGroup;CancelledError仅在当前await处抛出,不跨协程传播,易致资源泄漏。

嵌套异步任务的取消不会自动穿透到子协程,必须显式传递取消信号或依赖事件循环调度链;否则子任务会继续运行,造成资源泄漏或逻辑错乱。
asyncio.CancelledError 不会跨 await 自动传播到深层协程
调用 task.cancel() 后,CancelledError 只会在该 Task 当前挂起处(即最近一个 await 表达式)抛出,不会“穿透”进它 await 的另一个协程内部。比如:
async def inner():
await asyncio.sleep(5) # 这里不会因外层 cancel 而中断
<p>async def outer():
await inner() # outer 被 cancel → CancelledError 在这行抛出,inner 不受影响</p><p>task = asyncio.create_task(outer())
task.cancel()
await task # outer 抛 CancelledError,但 inner 仍在 sleep这是因为每个协程有独立的执行上下文和异常处理边界。outer 被取消 ≠ inner 被取消。
- outer 中的
await inner()是一个挂起点,取消只作用于 outer 的执行流 - inner 协程本身未被封装为 Task,也未被显式监听取消状态,因此完全 unaware
- 若 inner 持有数据库连接、文件句柄等资源,就可能泄漏
用 asyncio.shield() 或 contextvars 传递取消上下文
要让子协程感知父级取消,需主动把取消信号“带进去”。常见做法不是靠异常传播,而是靠状态检查或包装:
立即学习“Python免费学习笔记(深入)”;
- 在
inner开头加if asyncio.current_task().cancelled(): raise asyncio.CancelledError()—— 但不推荐,耦合重且易漏 - 更可靠的是用
asyncio.shield()包裹关键子操作,防止其被外层取消干扰(适用于必须完成的清理步骤) - 对深度嵌套场景,建议用
contextvars.ContextVar[asyncio.Task]存储当前可取消的 Task,并在子协程中定期检查task.cancelled() - 避免在子协程里直接
await无 Task 封装的协程;统一用asyncio.create_task()启动,再await task,这样取消才能落到具体 Task 实例上
TaskGroup 是解决嵌套取消的现代标准方案
Python 3.11+ 的 asyncio.TaskGroup 内置了结构化取消:任一子任务被取消或异常,整个组自动取消其余任务,并确保所有 __aexit__ 正常执行。
async def worker(name):
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
print(f"{name} 被取消")
raise
<p>async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(worker("A"))
tg.create_task(worker("B"))</p><h1>若 A 抛 CancelledError,B 会被自动 cancel,且 tg.<strong>aexit</strong> 保证执行它比 asyncio.gather() 更安全,因为:
-
gather(return_exceptions=True)只收集异常,不取消其他任务 -
TaskGroup的取消是协作式 + 自动传播,无需手动检查task.cancelled() - 即使某个子任务在
await前崩溃,TaskGroup仍能完成 cleanup
容易被忽略的关键点
最常踩的坑不是“怎么写取消逻辑”,而是忘了取消发生时的执行时机与资源生命周期错位:
-
finally块在取消时不一定执行——仅当取消发生在try块内await点才触发;若取消发生在acquire_resource()返回后、进入try前,finally就跳过了 - 用
async with替代手写try/finally,因为__aexit__在取消时仍会被调用(只要对象已成功进入async with) -
CancelledError继承自BaseException,所以except Exception:完全捕获不到,必须写except asyncio.CancelledError:或except BaseException:(不推荐后者) - 子协程里做 CPU 密集计算(如
sum(range(10**7)))时,即使外层已 cancel,它也会跑完——因为没await点让事件循环注入异常


















