Задачи: gather и create_task

Учимся запускать несколько корутин по-настоящему конкурентно через create_task и gather.

Task — корутина, обёрнутая для немедленного планирования в событийном цикле. gather — способ дождаться сразу нескольких awaitable и собрать их результаты.

Ошибка новичка: await по очереди

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

import asyncio

async def download(name, delay):
    await asyncio.sleep(delay)
    return f"{name} ({delay}с)"

async def main():
    # ПЛОХО: последовательно, суммарно 1 + 2 + 3 = 6 секунд
    a = await download("A", 1)
    b = await download("B", 2)
    c = await download("C", 3)
    print(a, b, c)

asyncio.run(main())

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

create_task: запустить сейчас

asyncio.create_task() сразу планирует корутину в цикле — она начинает выполняться, не дожидаясь, пока мы её «попросим». Это превращает корутину в конкурентную задачу.

async def main():
    t1 = asyncio.create_task(download("A", 1))
    t2 = asyncio.create_task(download("B", 2))
    t3 = asyncio.create_task(download("C", 3))
    # все три уже работают; теперь дожидаемся результатов
    print(await t1, await t2, await t3)

asyncio.run(main())

Теперь все три задержки идут одновременно, и общее время — около 3 секунд (самая долгая), а не 6.

gather: коротко и удобно

asyncio.gather() делает то же самое лаконичнее: принимает несколько awaitable, запускает их конкурентно и возвращает список результатов в порядке аргументов (не в порядке завершения).

async def main():
    results = await asyncio.gather(
        download("A", 1),
        download("B", 2),
        download("C", 3),
    )
    print(results)

asyncio.run(main())

Ожидаемый вывод на реальной машине (порядок сохраняется, время около 3 секунд):

['A (1с)', 'B (2с)', 'C (3с)']

Модель выигрыша по времени

Посчитаем разницу между последовательным и конкурентным вариантами на чистом stdlib — без asyncio, просто арифметикой задержек.

delays = [1, 2, 3]

sequential = sum(delays)   # await по очереди
concurrent = max(delays)   # gather / create_task

print("Последовательно, с:", sequential)
print("Конкурентно, с:", concurrent)
print("Сэкономлено, с:", sequential - concurrent)

Вывод:

Последовательно, с: 6
Конкурентно, с: 3
Сэкономлено, с: 3

Полезные детали gather

  • Результаты возвращаются в порядке аргументов, даже если задачи завершились в другом порядке.
  • Если одна корутина бросит исключение, gather по умолчанию пробросит его; параметр return_exceptions=True вернёт исключения как обычные элементы списка.
  • create_task удобнее, когда задаче нужно жить «в фоне», а результат заберём позже.

Итог

  • Подряд идущие await выполняются последовательно — выигрыша нет.
  • create_task сразу планирует корутину, запуская её конкурентно.
  • gather запускает несколько awaitable конкурентно и собирает результаты в порядке аргументов.
Проверьте себя
1. Сколько примерно времени займут три загрузки по 1, 2 и 3 секунды при await по очереди?
A3 секунды
B1 секунду
C6 секунд
D2 секунды
2. В каком порядке asyncio.gather(a, b, c) вернёт результаты?
AВ порядке завершения задач
BВ порядке аргументов: a, b, c
CВ случайном порядке
DВ обратном порядке: c, b, a
3. Что делает asyncio.create_task(coro)?
AСоздаёт новый процесс ОС
BСразу планирует корутину в событийном цикле, запуская её конкурентно
CВыполняет корутину синхронно до конца
DСоздаёт отдельный поток
Поддержать проект