Быстрый результат ждёт медленный: выдаём ответы через asyncio.as_completed

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

asyncio.as_completed позволяет ожидать следующие завершившиеся операции. На Python 3.12 обычный цикл получает coroutine ожидания очередного результата; каждую нужно await. Это отличается от gather, который собирает общий список в порядке переданных awaitable. Выбор определяется интерфейсом: нужен итоговый массив или постепенная выдача готовых ответов.

Порядок готовности результатов

Ниже медленная задача ждёт разрешения Event. Быстрая сразу возвращает значение. После первого результата управляющий код открывает Event. Если ошибочно перейти к ожиданию исходного списка задач, выполнение зависнет на первой. Такой пример показывает различие порядка без искусственно долгого sleep.

import asyncio

async def main():
    release = asyncio.Event()

    async def slow():
        await release.wait()
        return "slow"

    async def fast():
        return "fast"

    tasks = [asyncio.create_task(slow()), asyncio.create_task(fast())]
    results = []
    for completed in asyncio.as_completed(tasks):
        value = await completed
        results.append(value)
        if value == "fast":
            release.set()
    assert results == ["fast", "slow"]
    print(results)

asyncio.run(main())

Ошибки и оставшиеся задачи

Запуск на Python 3.12 печатает ['fast', 'slow']. Обратите внимание: результат несёт собственную идентичность. В прикладном коде полезно возвращать пару из ID и ответа, чтобы не сопоставлять ответ с позицией coroutine ожидания. Форма итерации as_completed менялась в новых версиях Python; пример намеренно использует интерфейс, доступный в 3.12.

Заранее выберите политику ошибок. Если await очередного результата поднимет исключение, обычный выход из цикла сам по себе не отменяет все остальные созданные задачи. Для независимых проверок можно записать ошибку одной операции и продолжить сбор. Для общего отказа нужно отменить остальные задачи и дождаться их завершения, чтобы очистка ресурсов действительно выполнилась.

Обработка по готовности не ограничивает конкурентность. Если передать миллион операций, проблема создания миллиона задач не исчезнет. Для большого источника нужен ограниченный пул или окно, которое добавляет новую работу после завершения прежней. as_completed выбирает порядок получения результатов среди уже запланированного набора.

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

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

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

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

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

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

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

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