为何asyncio.wait超时后仍保留任务引用引发内存泄漏?
问题复现
近期发现并复现了asyncio.wait引发的内存泄漏问题:程序通过循环定期执行逻辑,直到stop_event被触发,简化代码如下:
async def main(): stop_event = asyncio.Event() while True: # 执行业务逻辑 await asyncio.wait([stop_event.wait()], timeout=0.0001) asyncio.run(main())
运行后内存占用数分钟内飙升至数百MB,通过gc.get_referrers排查发现:每次调用asyncio.wait都会创建持有stop_event.wait()返回对象引用的任务,且这些任务会被永久保留,len(asyncio.all_tasks())持续增长;只有调用stop_event.set()时,所有任务才会结束,内存占用骤降。
解决方案
查阅官方文档得知:
Unlike wait_for(), wait() does not cancel the futures when a timeout occurs.
改用asyncio.wait_for后,内存泄漏问题消失,代码如下:
async def main(): stop_event = asyncio.Event() while True: # 执行业务逻辑 try: await asyncio.wait_for(stop_event.wait(), timeout=0.0001) except asyncio.TimeoutError: pass asyncio.run(main())
核心疑问解惑
1. 为何超时后要保留任务引用?
asyncio.wait的设计定位是将未完成的future控制权交还给调用者:它会返回(done, pending)两个集合,分别包含已完成和未完成的future。为了让你能对pending集合中的future进行后续操作(比如等待完成、手动取消),asyncio必须保留这些任务的引用——如果不保留,这些pending的任务可能会被垃圾回收,导致你无法再操作它们。因此超时后,那些等待stop_event的任务会被留在asyncio的活跃任务列表中,直到它们自然完成(stop_event.set())或被手动取消。
2. 若不取消future但不保留任务引用是否可行?
不可行。原因有两点:
- 若asyncio不保留任务引用,Python的垃圾回收机制可能会回收处于等待状态的任务,导致任务被意外终止,引发不可预测的行为;
- asyncio的任务调度器依赖内部任务列表跟踪所有活跃任务,若任务不在列表中,调度器无法对其进行管理,违背了异步任务的调度逻辑。
因此,只要任务处于未完成的活跃状态(包括等待事件),asyncio就必须保留其引用,直到任务完成或被取消。
内容的提问来源于stack exchange,提问作者s1m0n

