如何安全地取消Python协程的_task.cancel()并处理CancelledError异常及资源清理?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1008个文字,预计阅读时间需要5分钟。
由于`asyncio.Task.cancel()`并非温和提示取消,而是为协同程序打上已取消标记,并在下一个`await`点主动触发`CancelledError`——这是asyncio的协作式取消机制,而非强制中断。
协同程序必须自己响应这个异常,否则即使逻辑基本正常也不会退出。
常见错误现象:task.cancel() 调用了,但 finally 没执行、async with 没退出、socket 没 close、文件没 flush。
- 协程里没写
try/except CancelledError或没用finally - 在
await之前就卡死(比如死循环、CPU 密集型计算),永远等不到抛异常的时机 - 用了
loop.run_in_executor调用阻塞函数,取消对线程内执行无影响
如何安全退出并确保资源清理?
核心原则:把清理逻辑放在 finally 块里,或用 async with / async for 自动管理。Cancel 之后,协程会在下一个可挂起点被中断,然后流程自然进入 finally。
使用场景:HTTP 客户端连接、数据库连接池、临时文件写入、长轮询任务。
立即学习“Python免费学习笔记(深入)”;
示例:
async def fetch_data(): conn = None try: conn = await aiohttp.ClientSession() resp = await conn.get("https://httpbin.org/delay/5") return await resp.text() finally: if conn is not None: await conn.close() # 这行一定会执行
- 不要在
try中直接return,绕过finally - 避免在
finally里做新的await(除非你确认它不会被取消;否则可能再抛一次CancelledError) - 若清理本身可能耗时,考虑加超时:
asyncio.wait_for(cleanup(), timeout=2)
cancel() 调用后,task.done() 为 False 是正常吗?
是正常的。调用 task.cancel() 只是设置取消状态,真正完成要等协程响应并退出 —— 中间可能还有 await、finally、甚至嵌套的子任务等待。
性能 / 兼容性影响:如果协程在 finally 里做了重试、日志上传等耗时操作,task.done() 会延迟返回 True,但不会卡住事件循环(前提是没写死循环或阻塞调用)。
- 别用
while not task.done(): await asyncio.sleep(0.01)等待 —— 改用await task或asyncio.wait([task], return_when=asyncio.FIRST_COMPLETED) -
task.cancelled()返回True表示已被请求取消,不等于已完成 - 若想等清理结束再继续,就
await task;若不想阻塞,监听task的完成回调(add_done_callback)
CancelledError 被吞掉怎么办?
最常发生在你 catch 了 Exception 却没单独处理 CancelledError,或者用了第三方库的异常屏蔽逻辑(比如某些 retry 装饰器默认忽略所有异常)。
错误示例:
try: await do_something() except Exception: # ← 这里吞掉了 CancelledError log.error("failed")
- 显式捕获
asyncio.CancelledError并重新抛出(除非你真要压制它) - 用
except BaseException:要格外小心 —— 它包含CancelledError和KeyboardInterrupt - 检查所用异步库(如
aiofiles,aiomysql)是否支持取消;老版本可能忽略取消信号
复杂点在于:取消可能发生在任意 await 点,而清理逻辑又依赖当前协程上下文是否还有效。最容易被忽略的是——你以为 finally 万无一失,但若协程在 await 前崩溃(比如 raise 了别的异常),或取消发生在 __aexit__ 执行中途,资源仍可能泄漏。
本文共计1008个文字,预计阅读时间需要5分钟。
由于`asyncio.Task.cancel()`并非温和提示取消,而是为协同程序打上已取消标记,并在下一个`await`点主动触发`CancelledError`——这是asyncio的协作式取消机制,而非强制中断。
协同程序必须自己响应这个异常,否则即使逻辑基本正常也不会退出。
常见错误现象:task.cancel() 调用了,但 finally 没执行、async with 没退出、socket 没 close、文件没 flush。
- 协程里没写
try/except CancelledError或没用finally - 在
await之前就卡死(比如死循环、CPU 密集型计算),永远等不到抛异常的时机 - 用了
loop.run_in_executor调用阻塞函数,取消对线程内执行无影响
如何安全退出并确保资源清理?
核心原则:把清理逻辑放在 finally 块里,或用 async with / async for 自动管理。Cancel 之后,协程会在下一个可挂起点被中断,然后流程自然进入 finally。
使用场景:HTTP 客户端连接、数据库连接池、临时文件写入、长轮询任务。
立即学习“Python免费学习笔记(深入)”;
示例:
async def fetch_data(): conn = None try: conn = await aiohttp.ClientSession() resp = await conn.get("https://httpbin.org/delay/5") return await resp.text() finally: if conn is not None: await conn.close() # 这行一定会执行
- 不要在
try中直接return,绕过finally - 避免在
finally里做新的await(除非你确认它不会被取消;否则可能再抛一次CancelledError) - 若清理本身可能耗时,考虑加超时:
asyncio.wait_for(cleanup(), timeout=2)
cancel() 调用后,task.done() 为 False 是正常吗?
是正常的。调用 task.cancel() 只是设置取消状态,真正完成要等协程响应并退出 —— 中间可能还有 await、finally、甚至嵌套的子任务等待。
性能 / 兼容性影响:如果协程在 finally 里做了重试、日志上传等耗时操作,task.done() 会延迟返回 True,但不会卡住事件循环(前提是没写死循环或阻塞调用)。
- 别用
while not task.done(): await asyncio.sleep(0.01)等待 —— 改用await task或asyncio.wait([task], return_when=asyncio.FIRST_COMPLETED) -
task.cancelled()返回True表示已被请求取消,不等于已完成 - 若想等清理结束再继续,就
await task;若不想阻塞,监听task的完成回调(add_done_callback)
CancelledError 被吞掉怎么办?
最常发生在你 catch 了 Exception 却没单独处理 CancelledError,或者用了第三方库的异常屏蔽逻辑(比如某些 retry 装饰器默认忽略所有异常)。
错误示例:
try: await do_something() except Exception: # ← 这里吞掉了 CancelledError log.error("failed")
- 显式捕获
asyncio.CancelledError并重新抛出(除非你真要压制它) - 用
except BaseException:要格外小心 —— 它包含CancelledError和KeyboardInterrupt - 检查所用异步库(如
aiofiles,aiomysql)是否支持取消;老版本可能忽略取消信号
复杂点在于:取消可能发生在任意 await 点,而清理逻辑又依赖当前协程上下文是否还有效。最容易被忽略的是——你以为 finally 万无一失,但若协程在 await 前崩溃(比如 raise 了别的异常),或取消发生在 __aexit__ 执行中途,资源仍可能泄漏。

