asyncio.run() 中协程被取消时 finally 和 aexit 可能不执行,导致资源泄漏;应改用 create_task()+显式await、gather(return_exceptions=True)、定期检查取消标志、用shield保护关键清理,并验证第三方库的取消安全性。

asyncio.run() 里没执行 finally 或 __aexit__ 怎么办
这是最常见的资源泄漏源头:用 asyncio.run() 启动协程时,如果协程被取消(比如 Ctrl+C、超时、任务异常退出),finally 块或异步上下文管理器的 __aexit__ 可能根本不会运行。
根本原因是 asyncio.run() 在遇到未处理的取消异常(CancelledError)时会直接终止事件循环,跳过协程的正常退出路径。
- 别在顶层协程里依赖
finally做清理 —— 改用asyncio.create_task()+ 显式await task,并在外层加try/except CancelledError - 用
async with时,确保上下文管理器本身在__aexit__中处理了取消 —— 比如调用await self._cleanup()前加if not task.cancelled(): - 对关键资源(如数据库连接、文件句柄),在
__aexit__开头就记录日志,确认它是否被调用;若没日志,基本可断定被跳过了
多任务并发下 await asyncio.gather() 的 cleanup 陷阱
asyncio.gather() 默认行为是“任一子任务失败即取消其余”,但这个取消是静默的 —— 被取消的任务不会自动触发其内部的清理逻辑,除非你显式捕获并处理。
典型现象:启动 5 个数据库查询任务,第 3 个抛出 TimeoutError,其余 2 个还在跑,但 gather() 返回后它们就被丢弃了,连接没关,连接池慢慢耗尽。
立即学习“Python免费学习笔记(深入)”;
- 改用
asyncio.gather(*tasks, return_exceptions=True),拿到所有结果(含异常),再统一处理每个任务的清理 - 每个子任务自己包装成独立函数,并在最外层加
try/finally或async with—— 不要指望gather()替你兜底 - 避免在
gather()里传入已带async with的协程 —— 因为上下文管理器生命周期只绑定到该协程本身,不是整个gather()调用
信号处理(SIGINT/SIGTERM)与协程取消的时序错位
在生产环境用 uvicorn 或自建服务时,收到 SIGINT 后主协程被取消,但此时正在运行的子任务可能刚进入 I/O 等待,还没来得及响应取消信号,导致清理代码永远等不到执行机会。
1.修正BUG站用资源问题,优化程序2.增加关键词搜索3.修改报价4.修正BUG 水印问题5.修改上传方式6.彻底整合论坛,实现一站通7.彻底解决群发垃圾信息问题。注册会员等发垃圾邮件7.彻底解决数据库安全9.修改交易方式.增加网站担保,和直接交易两中10.全站可选生成html.和单独新闻生成html(需要装组建)11. 网站有10中颜色选择适合不同的行业不同的颜色12.修改竞价格排名方式13.修
这不是 Python bug,而是异步取消的固有特性:取消只是设置一个标志,协程需主动检查(如通过 await asyncio.sleep(0) 或在 await 点响应)。
- 在长循环中定期插入
if asyncio.current_task().cancelled(): break,尤其在 CPU 密集型处理段之后 - 用
asyncio.shield()包裹真正不可中断的清理操作(如关闭 TCP 连接),防止它被外部取消打断 - 注册信号处理器时,不要直接
loop.stop(),而应asyncio.create_task(shutdown()),让shutdown()协程按需等待子任务完成
第三方库(如 httpx、aiomysql)的 async context manager 是否真可靠
很多库文档写着“支持 async with”,但实际实现里 __aexit__ 可能没处理 CancelledError,或者清理逻辑本身也 await 了不响应取消的底层调用(比如某些 SSL 关闭过程)。
验证方法很简单:手动取消任务,看连接是否从 netstat -an | grep :port 里消失,或观察连接池的 idle 数是否归零。
- 优先选明确声明“cancel-safe cleanup”的库版本 —— 比如
httpx>=0.24.0修复了AsyncClient在取消时可能卡住的问题 - 对关键客户端,封装一层带超时的清理:用
asyncio.wait_for(self._client.aclose(), timeout=3.0) - 不要复用跨请求的
AsyncClient实例做长期连接管理 —— 它的设计本意是短生命周期,长期持有反而增加清理不确定性
资源清理不是写完 async with 就万事大吉的事。异步取消的传播是协作式的,每个 await 点都可能是清理逻辑的断点。最容易被忽略的是:你以为在清理,其实协程早已被扔进垃圾堆,连入口函数都没走到。









