CancelledError 继承自 BaseException 而非 Exception,需显式捕获或用 try/finally、async with 保证清理;长任务应主动检查 cancelled();await 已取消 task 会传播异常,宜用 shield 或 done callback。

CancelledError 不是普通异常,不能靠 try/except 无差别捕获
很多人在 asyncio 中遇到任务被取消时想“做点收尾”,第一反应是写 except Exception: 或 except BaseException:,结果发现 CancelledError 被吞掉或根本没进 except 块。这是因为从 Python 3.8 开始,CancelledError 继承自 BaseException(而非 Exception),且 asyncio 在取消任务时会直接 raise 它并跳过常规异常处理链——除非你显式捕获它,或者用 finally / async with / async for 等结构触发清理。
必须用 try/finally 或 async context manager 包裹可取消的临界区
清理逻辑要可靠执行,唯一稳当的方式是把资源操作放在 try 块里,把释放/回滚动作放在对应的 finally 块中。异步资源(如数据库连接、HTTP 会话)则优先用 async with,它底层会调用 __aexit__,即使中途被取消也会触发:
async def upload_file():
client = AsyncHTTPClient()
try:
await client.post("/upload", data=...)
return "success"
finally:
# 这里一定会执行,哪怕 upload 被 cancel
await client.close()
-
await在finally里是允许的,但要注意:如果await client.close()本身也被取消,就会再次抛CancelledError,可能中断清理——所以关键清理步骤应尽量避免再 await 可取消协程 - 若必须 await,应在内部加
try/except CancelledError并确保最终状态一致(比如标记连接为“已关闭”,不再重复 close) - 不要依赖
__del__或弱引用回调做清理,它们不保证执行时机,更不保证在取消时触发
主动检查 cancellation 并提前退出,避免无效 work
长耗时协程(如大文件读取、密集计算)不能只靠外部取消信号被动响应;应该在循环或分段逻辑中主动检查任务是否已被取消,及时停止:
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
async def process_large_stream(reader):
while True:
# 检查是否已被取消,避免继续读取
if asyncio.current_task().cancelled():
break
chunk = await reader.read(8192)
if not chunk:
break
await handle_chunk(chunk)
- 用
asyncio.current_task().cancelled()判断比捕获CancelledError更早、更轻量 - 对 CPU-bound 操作,需配合
asyncio.to_thread()并在子线程内定期轮询task.cancelled()(主线程无法中断线程,只能协作退出) - 若使用
asyncio.wait_for(),超时后抛出的是TimeoutError,不是CancelledError,别混淆两者的处理路径
cancel() 后 await task 是危险操作,需用 shield 或 done callback
直接 await task 一个已被 cancel 的 task,会立刻 re-raise CancelledError,导致调用方也中断。如果你只是想确认它结束了(不管是否取消),应该用 task.done() 配合 asyncio.shield() 或监听完成回调:
- 错误写法:
await task→ 可能炸掉上层协程 - 安全写法:
await asyncio.shield(task)→ 但 shield 不阻止取消,只是让 await 不立即抛错;实际仍需在 task 内部处理好清理 - 更推荐:
task.add_done_callback(lambda t: print("done:", t.cancelled())),然后主逻辑继续跑,不 await - 如果必须等结束且不想传播取消,可用
await asyncio.wait([task], return_when=asyncio.FIRST_COMPLETED)
真正难的不是写 cleanup,而是判断哪些状态需要 rollback:比如事务已提交就不能撤回,只能走补偿逻辑;而本地缓存、临时文件、未 flush 的 buffer 才适合在 finally 里清除。这取决于你的业务语义,不是框架能替你决定的。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










