asyncio.cancel() 不能强制终止协程,仅设取消标记并在下次 await 时抛 CancelledError;需协程主动配合(如插入 await、检查 cancelled())才能真正响应取消。

asyncio.cancel() 能否真正终止正在运行的协程
asyncio.cancel() 并不会强制中断协程执行,它只是给 Task 设置一个取消标记,并在下一次 await 时抛出 CancelledError。如果协程里全是 CPU 密集型计算、没任何 await,那取消就完全不生效。
常见错误现象:调用 task.cancel() 后,任务仍在后台跑满 CPU,日志里也看不到异常。
- 必须确保协程中存在可取消的挂起点,比如
await asyncio.sleep(1)、await aiohttp.get(...)、await queue.get() - 长时间计算逻辑要主动插入
await asyncio.sleep(0)或await asyncio.shield(...)(慎用)来让出控制权 - 捕获
CancelledError后,通常应直接返回或清理资源,不要“吞掉”后继续执行
如何安全地在 cancel 后释放资源(如文件句柄、连接)
协程被取消时,CancelledError 是继承自 BaseException 的,所以普通 except Exception: 捕获不到,必须显式处理。
使用场景:异步写文件、维持 WebSocket 连接、持有数据库连接池租约等。
立即学习“Python免费学习笔记(深入)”;
- 用
try/except CancelledError:包裹关键清理逻辑,或更推荐用async with+ 支持异步__aexit__的上下文管理器 - 避免在
finally块里做耗时 await 操作(如await db.close()),因为此时事件循环可能已关闭;可改用loop.create_task()延迟调度 - 若清理本身也可能被取消(比如
await redis.connection_pool.disconnect()超时),需加超时控制:await asyncio.wait_for(cleanup(), timeout=2.0)
asyncio.gather() 中部分任务被取消时的异常传播行为
asyncio.gather() 默认遇到任意子任务异常(包括 CancelledError)就立即停止并抛出,但具体抛什么,取决于 return_exceptions 参数。
参数差异:
-
return_exceptions=False(默认):只要有一个任务被取消,整个gather就 raiseCancelledError,其余任务状态不确定(可能还在跑) -
return_exceptions=True:被取消的任务返回CancelledError实例而非抛出,其他任务继续运行,最终结果是混合列表 - 注意:即使设了
return_exceptions=True,主协程仍可能因父级取消而中断,不能依赖它“保底执行完”
示例:results = await asyncio.gather(task_a, task_b, return_exceptions=True) —— 若 task_a 被取消,results[0] 是 CancelledError 实例,results[1] 是 task_b 的返回值。
取消信号从上层传入深层协程的常用模式
深层协程(比如嵌套三层 await)无法自动感知外层 Task 是否被取消,必须显式传递取消上下文或检查 asyncio.current_task().cancelled()。
容易踩的坑:写了个工具函数 fetch_with_retry(),内部重试逻辑没检查取消状态,导致即使外层已 cancel,它还在傻等重试间隔。
- 推荐方式:把
asyncio.Task或asyncio.CancelScope(来自 anyio)作为参数传入,或使用asyncio.shield()显式保护不可取消段 - 轻量检查:在循环或重试开头加
if asyncio.current_task().cancelled(): raise asyncio.CancelledError() - 避免用
time.sleep()或while True:死循环,它们不响应取消;改用await asyncio.sleep()并配合取消检查
复杂点在于,取消不是“硬杀”,而是协作式中断 —— 每一层都要愿意停下来,否则信号就断在半路了。










