Производитель быстрее потребителя: ограничиваем буфер asyncio.Queue

Приложение читает задания из потока и отправляет их медленному обработчику. Пока нагрузка мала, список ожидающих выглядит безобидно. Когда источник ускоряется, в памяти накапливаются сами задания и связанные с ними данные. Ограничение числа одновременно работающих обработчиков не ограничивает число уже созданных ожидающих задач.

Очередь с maxsize задаёт размер промежуточного буфера. Когда он заполнен, await queue.put ждёт, пока потребитель заберёт элемент. Ожидание замедляет производителя в той же цепочке исполнения. Так скорость чтения начинает зависеть от доступной ёмкости, а не от желания заранее поставить в очередь весь источник.

Очередь с ограниченной ёмкостью

Пример ниже делает границу видимой без измерения памяти. Очередь вмещает один элемент. Второй put запускается задачей и не завершается, пока первый элемент не извлечён. Event сообщает, что производитель уже дошёл до попытки записи. Это позволяет проверить ожидание без случайной большой задержки.

import asyncio

async def main():
    queue = asyncio.Queue(maxsize=1)
    started = asyncio.Event()
    await queue.put("first")

    async def produce():
        started.set()
        await queue.put("second")

    producer = asyncio.create_task(produce())
    await started.wait()
    assert not producer.done()
    first = await queue.get()
    await producer
    second = await queue.get()
    assert (first, second) == ("first", "second")
    print(first, second)

asyncio.run(main())

Где снова появляется накопление

Запуск Python 3.12 печатает first second. После started.wait задача производителя дошла до блокирующего put, но ещё не завершилась. get освобождает место, и второе значение попадает в очередь. Здесь проверяется именно вместимость буфера; учёт завершённой работы через task_done и join — другой контракт, его стоит проверять отдельно.

Чтобы ограничение работало в приложении, производитель должен действительно ожидать put. Если для каждого входного элемента создавать create_task(queue.put(item)), ожидающие coroutine сами станут неограниченным буфером. Значения уже прочитаны, задачи уже созданы, и maxsize регулирует только число элементов внутри Queue. Читайте источник последовательно либо используйте ограниченное число производителей.

Выбор maxsize зависит от размера элемента и допустимого времени ожидания. Сто тысяч маленьких ключей и сто тысяч загруженных документов требуют разной памяти. Если источник позволяет получать крупные страницы, ограничьте и размер страницы: очередь не контролирует буфер внутри клиента. В наблюдаемость полезно добавить время ожидания put, чтобы видеть отставание обработки.

При остановке заранее определите судьбу ожидающих производителей и оставшихся элементов. Нельзя просто забыть задачу, которая ждёт места: она останется живой. Управляющий код должен завершить производителей, дождаться нужной работы и остановить потребителей выбранным протоколом. Ограниченная очередь решает накопление между двумя этапами, когда оба этапа участвуют в этом протоколе.

Когда локальный пример работает, полезно разобрать соседние механизмы языка и их взаимодействие. В курсе «Python для профессионалов» есть практика по итераторам, генераторам, декораторам, asyncio, метапрограммированию и профилированию. Эти темы помогают понимать время жизни объектов, управление выполнением и причины поведения прикладной программы.

Посмотреть программу и практику курса.

Документация: официальный источник по теме.

Другие задачи по теме: Типы, тесты и поведение программы.

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.