8.4. Gather

У попередньому розділі показано, як asyncio.create_task() планує корутину і не чекає на неї. Супутня операція – запустити кілька awaitable одночасно і чекати на всі – це asyncio.gather().

8.4.1. Базова форма

import asyncio

async def fetch(name, delay_ms):
    await asyncio.sleep_ms(delay_ms)
    return name

async def main():
    results = await asyncio.gather(
        fetch("a", 100),
        fetch("b", 200),
        fetch("c", 50),
    )
    print(results)

asyncio.run(main())

Output:

['a', 'b', 'c']

Два важливих спостереження. По-перше, список результатів упорядкований відповідно до аргументів gather, а не у порядку завершення корутин – "c" завершилась першою, але все одно є третім записом. По-друге, виклик тривав 200 ms загалом, а не 350 ms: три сни виконувалися одночасно, і gather() повертається, щойно завершується найповільніший.

Обидва факти мають одне джерело. gather() обгортає кожен аргумент, який ще не є задачею (корутини в прикладі), планує їх у циклі, призупиняє корутину, що викликала, до завершення всіх з них, а потім повертає їхні результати в початковому порядку.

8.4.2. Коли варто використовувати

Скрізь, де програма має N awaitable операцій і хоче отримати результати всіх перед продовженням. Типові приклади:

  • Паралельне виконання кількох мережевих запитів і очікування всіх відповідей.

  • Одночасне зчитування з кількох датчиків перед обробкою об’єднаного результату.

  • Об’єднання кількох короткочасних допоміжних задач наприкінці фази програми.

Це не правильний інструмент для довготривалих фонових задач, які програма запускає та залишає працювати протягом усього часу – для них як і раніше використовується create_task(). gather() призначений для шаблону розгалуження / злиття: розділити роботу, виконати паралельно, об’єднати.

8.4.3. Винятки в групі

Якщо будь-який з gathered awaitable генерує виняток, стандартна поведінка – повторно генерувати виняток за межі виклику gather. Сусідні задачі, що ще не завершилися, скасовуються у фоні.

Зазвичай це те, чого хоче програма – одна з N паралельних задач зазнала невдачі, тому спільна операція зазнала невдачі, тому не витрачайте час на решту. Іноді програма хоче протилежного: дозволити кожному awaitable завершитися (або зазнати невдачі) незалежно, а потім перевірити результати. Для цього передайте return_exceptions=True

results = await asyncio.gather(
    fetch_or_fail("ok"),
    fetch_or_fail("bad"),
    return_exceptions=True,
)
# results == ["ok-value", OSError(...)]

Тепер кожен запис у поверненому списку є або звичайним поверненим значенням, або винятком, який згенерував відповідний awaitable. Виклик перевіряє isinstance(r, Exception), щоб розрізнити їх.

8.4.4. Скасування

Скасування самого gather – через скасування будь-якої задачі, яка його очікувала – скасовує кожен awaitable, що ще виконується всередині нього. Сторінка тайм-аути та скасування детально описує, як скасування поширюється по ланцюжку викликів.