Python asyncio 长运行服务中任务为何会导致内存持续增长?
结论
示例中的内存持续增长,是因为全局列表 tasks 一直保存着每个 Task 对象。任务完成后,列表仍保留对它的引用,因此任务对象及其结果无法被垃圾回收。示例每 0.5 秒创建一个任务,却从不移除已完成的任务,列表便会不断变大。
内存增长的原因是任务引用没有及时清理。管理任务时,应保留仍在运行的任务,在任务结束后移除引用,并处理任务异常和服务关闭。
管理后台任务
可以用集合追踪尚未完成的任务,在任务结束时将其移除并读取异常。服务关闭时,取消剩余任务并等待它们结束:
import asyncio
import logging
logging.basicConfig(level=logging.INFO)
async def worker(n):
await asyncio.sleep(1)
return n * 2
async def scheduler():
pending = set()
def on_done(task):
pending.discard(task)
if task.cancelled():
return
exc = task.exception()
if exc is not None:
logging.error(
"后台任务失败",
exc_info=(type(exc), exc, exc.__traceback__),
)
try:
while True:
task = asyncio.create_task(worker(10))
pending.add(task)
task.add_done_callback(on_done)
await asyncio.sleep(0.5)
finally:
remaining = tuple(pending)
for task in remaining:
task.cancel()
await asyncio.gather(*remaining, return_exceptions=True)
async def main():
await scheduler()
asyncio.run(main())
on_done 会在任务结束时将它从集合中移除,并调用 task.exception() 读取异常,避免任务失败被忽略。调度器退出或被取消时,finally 会清理仍在运行的任务。如果希望由调用方统一处理任务失败,也可以等待任务并读取结果,而不是只在回调中记录异常。
让并发量有上限
如果任务的处理时间可能超过调度间隔,持续调用 create_task() 会让同时运行的任务越来越多。即使已完成的任务都能及时移除,工作积压仍可能让服务占用更多内存。对于持续输入的工作负载,通常应设置并发上限,例如用 asyncio.Queue 配合固定数量的 worker。队列也应设置容量,并明确队列满时如何处理。
如果任务是一组有限的子任务,可以考虑用 asyncio.TaskGroup 管理。无限调度循环则需要考虑并发限制和退出时的清理。
检查内存是否真的泄漏
Python 对象不再被引用后可以被垃圾回收,但进程的 RSS 不一定会立刻下降。内存分配器可能会保留已释放的内存,供后续分配使用。可以结合 tracemalloc 比较不同时间点的快照,确认仍被引用的 Python 对象是否持续增加,同时观察未完成任务数和待处理队列长度。强制垃圾回收无法释放仍被 tasks 等容器引用的对象。
备注:内容仅供参考。
