KernelRanch
返回文章列表

异步任务越跑内存越大:用有界队列给生产者背压

2026-11-12 #python#asyncio#queues#性能

给十万条输入各创建一个任务,即使任务内部有信号量限制并发,等待中的任务仍占内存。使用固定数量的 worker 和有界队列,队列满时让生产者等待,能把尚未处理的数据控制在明确的范围内。

1. 先证明队列满时生产者会停下

保存为 queue_lab.py,运行 python queue_lab.py。需要 Python 3.11 的 TaskGroup。

import asyncio

async def main():
    queue = asyncio.Queue(maxsize=2)
    await queue.put(1)
    await queue.put(2)
    attempted = asyncio.Event()

    async def put_third():
        attempted.set()
        await queue.put(3)

    task = asyncio.create_task(put_third())
    await attempted.wait()
    assert not task.done()
    assert await queue.get() == 1
    queue.task_done()
    await task
    assert queue.qsize() == 2
    print("full queue: producer waited; one read released it")

    jobs = asyncio.Queue(maxsize=4)
    total = 0
    processed = 0

    async def worker():
        nonlocal total, processed
        while True:
            item = await jobs.get()
            try:
                if item is None:
                    return
                await asyncio.sleep(0)
                total += item * item
                processed += 1
            finally:
                jobs.task_done()

    async with asyncio.TaskGroup() as group:
        for _ in range(3):
            group.create_task(worker())
        for item in range(100):
            await jobs.put(item)
        for _ in range(3):
            await jobs.put(None)
        await jobs.join()

    assert processed == 100 and total == 328350
    print("processed=100; sum=328350; workers=3; queue_capacity=4")

asyncio.run(main())

2. 容量预算怎么算

数据所在位置本例上限
队列中等待的工作项4
worker 已取出且尚未完成的工作项3
生产者等待 put 的当前工作项1

因此,在输入按需生成、worker 不另行堆积任务的条件下,等待和处理中的工作项最多约 8 个。队列限制的是条数。每条内容大小差异很大时,还需要限制单项字节数。示例将结果累加到计数器,若把所有输出追加到列表,输出端仍会随总任务数增长。

第一组实测生产者停在第三次 put,消费者取走一项后它完成。第二组处理 100 项,总和为 328350。await sleep(0) 只用于模拟让出执行权,实际 worker 中应等待网络或磁盘操作。

3. 处理异常和停机

TaskGroup 中任一 worker 抛出异常,会取消同组剩余任务,并打断组内正在等待的生产者。退出时异常向调用方传播。本例采用整批失败语义,已完成的外部副作用需要业务层自行处理。

task_done 要与每次成功 get 配对,包括结束标记。它用于减少未完成计数,即使在 finally 中调用,也不能作为业务成功标记。需要重试的任务应单独记录失败结果,并设置重试上限。

正常停机时,生产者先停止接收新输入,再给每个 worker 投递一个结束标记,等待队列排空和 worker 结束。设置停机总预算后,超时取消剩余任务。需要进程崩溃后继续处理的工作,应存入持久队列并定义确认和重试协议。

小结

同时限制队列容量和 worker 数量,让生产者在积压时等待。验收时检查输入、处理中数据和输出三处是否都能保持容量上限。

数据来源与检索时间

检索时间 2026-11-12。

来源URL对应内容
Python 3.11 官方文档https://docs.python.org/3.11/library/asyncio-queue.html有界 Queue、put、join 和 task_done
Python asyncio 任务文档https://docs.python.org/3.11/library/asyncio-task.html#task-groupsTaskGroup 的异常传播和取消

核验说明

  • 主示例在本地 Python 3.11.9 下执行,检查正文中的断言与输出。
  • 实验范围是文中构造的输入和本地运行条件,部署时需按实际数据规模与运行环境核验。