Как в Python asyncio запустить несколько корутин конкурентно и дождаться их завершения

Ответ

Для конкурентного запуска нескольких корутин и сбора их результатов используется высокоуровневая функция asyncio.gather().

Она принимает на вход одну или несколько корутин, планирует их выполнение в цикле событий и возвращает Future, который завершается, когда все переданные корутины отработали. Результатом является список возвращаемых значений корутин в том же порядке, в котором они были переданы.

Ключевые особенности:

  • Простота использования: Идеально подходит для сценария "запустить всё и дождаться всех".
  • Сбор результатов: Автоматически собирает и упорядочивает результаты.
  • Обработка ошибок: По умолчанию, если одна из корутин вызывает исключение, gather() немедленно прерывается и пробрасывает это исключение. Поведение можно изменить, установив флаг return_exceptions=True, тогда исключения будут возвращены в списке результатов наравне с успешными значениями.

Пример:

import asyncio

async def fetch_data(source: str, delay: int) -> str:
    print(f"Начинаю загрузку из {source}...")
    await asyncio.sleep(delay)
    result = f"Данные из {source}"
    print(f"Загрузка из {source} завершена.")
    return result

async def main():
    # Запускаем обе корутины конкурентно
    results = await asyncio.gather(
        fetch_data("API", 2),
        fetch_data("DB", 1)
    )
    print(f"nВсе результаты: {results}")

# Вывод покажет, что DB завершит работу раньше, 
# но gather дождется завершения API.
# >> Начинаю загрузку из API...
# >> Начинаю загрузку из DB...
# >> Загрузка из DB завершена.
# >> Загрузка из API завершена.
# >> 
# >> Все результаты: ['Данные из API', 'Данные из DB']

asyncio.run(main())

В отличие от asyncio.create_task(), которая только планирует выполнение корутины и немедленно возвращает объект Task, asyncio.gather() представляет собой более удобный способ для одновременного ожидания группы задач.

Ответ 18+ 🔞

А, слушай, смотри, есть же такая штука, чтобы кучу асинхронных дел разом запустить и не сойти с ума, пока ждёшь. Называется asyncio.gather(). Это типа как взять всех своих друзей-корутин и сказать: «Ну-ка, все нахуй, делайте своё дело, а я тут посижу, пока вы не закончите».

Вот в чём прикол:

  • Просто до безобразия: Кинул туда список корутин — и забыл. Вернётся тебе аккуратный список результатов, в том же порядке, в котором ты их туда пихал. Красота, блядь.
  • Собирает всё в кучу: Не нужно самому бегать и собирать результаты из каждой задачи — всё само приплывёт.
  • С ошибками: Тут, конечно, есть нюанс. Если одна из этих корутин возьмёт и накосячит, выкинув исключение, то по умолчанию gather() сразу же пизданутся и тебе эту ошибку в лицо бросит. Но если ты не идиот и хочешь получить всё, даже косяки, то поставь флаг return_exceptions=True. Тогда исключения просто будут лежать в списке результатов, как ни в чём не бывало. Хитрая жопа, но удобно.

Смотри, как это выглядит на практике:

import asyncio

async def fetch_data(source: str, delay: int) -> str:
    print(f"Начинаю загрузку из {source}...")
    await asyncio.sleep(delay)
    result = f"Данные из {source}"
    print(f"Загрузка из {source} завершена.")
    return result

async def main():
    # Запускаем обе корутины конкурентно
    results = await asyncio.gather(
        fetch_data("API", 2),
        fetch_data("DB", 1)
    )
    print(f"nВсе результаты: {results}")

# Вывод покажет, что DB завершит работу раньше, 
# но gather дождется завершения API.
# >> Начинаю загрузку из API...
# >> Начинаю загрузку из DB...
# >> Загрузка из DB завершена.
# >> Загрузка из API завершена.
# >> 
# >> Все результаты: ['Данные из API', 'Данные из DB']

asyncio.run(main())

И главное, запомни разницу, а то все путаются. Есть ещё asyncio.create_task() — она просто швыряет корутину в цикл событий и говорит «делай что хочешь», возвращая тебе объект Task. А gather() — это уже высший пилотаж, когда тебе надо не просто запустить, а ещё и дождаться всех этих пидарасов и собрать с них урожай. Удобно, ёпта.