python异步任务中需区分两类问题:一是循环变量在闭包中被意外共享(逻辑错误),如for循环中lambda引用i导致全输出2,应使用默认参数固化值;二是task持有协程帧引发内存滞留(真正泄漏),需及时await/cancel task、避免强引用、用weakref监控,并确保async with正确释放资源。

Python 异步任务中的“变量作用域泄漏”并不是标准术语,真正需要关注的是两类密切相关的问题:循环变量在闭包中被捕获时的意外共享(常被误称为“泄漏”),以及协程或 Task 持有外部变量导致的内存滞留(真正的资源泄漏)。两者都表现为变量“不该存在时还在”,但成因和解法完全不同。
循环变量在异步回调中意外复用
for 循环本身不创建作用域,变量名在循环结束后仍存在,且所有闭包(如 lambda、协程内嵌函数)若引用该变量,会共享最后一次赋值——这不是内存泄漏,而是逻辑错误,但在异步场景下尤为危险。
- 在
for i in range(3): asyncio.create_task(some_coro(i))中直接使用i通常安全,因为参数已传入 - 但若写成
for i in range(3): asyncio.create_task(asyncio.sleep(1, result=i))或tasks.append(lambda: print(i)),就可能全部输出2
推荐做法:
- 避免在循环体内定义依赖循环变量的闭包
- 必须使用时,用默认参数固化当前值:
lambda x=i: print(x) - 异步任务中优先显式传参,而非捕获外层变量
协程帧与 Task 持有变量引发的内存滞留
这才是真正的“泄漏”:协程挂起后,其栈帧(cr_frame)会持续持有局部变量、闭包引用、甚至大对象(如 DataFrame、响应体),即使任务已完成,只要 Task 对象未被回收,这些引用就无法释放。
-
asyncio.create_task(coro)后未 await、未 cancel,Task 会留在asyncio.all_tasks()中 - 即使 Task 已
done(),若被全局列表、日志extra、信号监听器等强引用,协程帧仍驻留 -
task.cancel()后若未处理异常,task.exception()绑定的 traceback 会拖住整条调用栈
关键检查点:
- 在关键路径添加诊断代码:
for t in asyncio.all_tasks(): if t.done() and not t.cancelled(): coro = t.get_coro() if hasattr(coro, 'cr_frame') and coro.cr_frame: # 检查 locals 是否含大对象 print("Done but frame alive:", list(coro.cr_frame.f_locals.keys())) - 避免在 Task 内部直接引用大对象;改用 ID、弱引用或延迟加载
- 使用
weakref.WeakSet()跟踪所有 Task,定期检查数量是否持续增长
上下文管理器未生效导致资源绑定泄漏
异步资源(如 aiohttp.ClientSession、数据库连接)若未用 async with 包裹,或 __aexit__ 未执行,其内部 Task 和 socket 句柄会长期存活,间接让调用它的协程变量无法释放。
- 错误写法:
session = aiohttp.ClientSession(); await session.get(...)—— 缺少await session.close() - 正确写法:始终
async with aiohttp.ClientSession() as session: - 第三方库需确认文档要求:
aiomysql.Pool要await pool.close(),redis.Redis要await redis.aclose()
闭包与 contextvars 的隐式生命周期延长
contextvars 本身不会泄漏,但若在 Task 中设置变量后未清理,或通过闭包长期持有 ContextVar 实例,可能导致上下文对象无法回收。
- 不要将
ContextVar实例存为类属性或全局变量 - 避免在
async with外部调用var.set()并期望自动重置 - 如需跨 Task 传递数据,优先用参数显式传递,而非依赖 contextvars
不复杂但容易忽略











