Короткий ответ
asyncio.gather запускает все переданные корутины конкурентно и ждёт завершения всех, так что суммарное время определяется самой долгой задачей; последовательный await выполняет их одну за другой, и суммарное время равно сумме времён всех задач.
Как это работает подробнее
await coro1(); await coro2() сначала полностью завершает coro1, и только затем начинает coro2 — конкурентности нет. asyncio.gather(coro1(), coro2()) оборачивает обе в задачи и позволяет event loop переключаться между ними, так что их ожидания перекрываются по времени.
Пример кода
example-269.py
import asyncio, time
async def fetch(delay, name):
await asyncio.sleep(delay)
return name
async def sequential():
start = time.perf_counter()
a = await fetch(1, "A")
b = await fetch(1, "B")
print("sequential:", time.perf_counter() - start) # ~2с
async def concurrent():
start = time.perf_counter()
a, b = await asyncio.gather(fetch(1, "A"), fetch(1, "B"))
print("concurrent:", time.perf_counter() - start) # ~1с
asyncio.run(sequential())
asyncio.run(concurrent())Что использовать на практике
- Практика.
gatherполезен для нескольких независимых I/O-bound операций; нужно аккуратно обрабатывать исключения — по умолчаниюgatherпрерывается при первом исключении, если не переданreturn_exceptions=True.
Подводные камни
- Риски. Используют последовательные
awaitтам, где задачи независимы, упуская возможность ускорения; забывают проreturn_exceptions=True, когда нужно собрать результаты всех задач независимо от того, упала ли одна из них.