Задачи: 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 конкурентно и собирает результаты в порядке аргументов.