Python 异步任务取消与异常处理

8次阅读

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

Python 异步任务取消与异常处理

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(默认):只要有一个任务被取消,整个 gatherraise CancelledError,其余任务状态不确定(可能还在跑)
  • 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.Taskasyncio.CancelScope(来自 anyio)作为参数传入,或使用 asyncio.shield() 显式保护不可取消段
  • 轻量检查:在循环或重试开头加 if asyncio.current_task().cancelled(): raise asyncio.CancelledError()
  • 避免用 time.sleep()while True: 死循环,它们不响应取消;改用 await asyncio.sleep() 并配合取消检查

复杂点在于,取消不是“硬杀”,而是协作式中断 —— 每一层都要愿意停下来,否则信号就断在半路了。

text=ZqhQzanResources