Корутины и цикл событий
async def создаёт корутину. await — точка, где она отдаёт управление циклу событий, пока ждёт сеть, диск или таймер. Цикл в это время выполняет другие корутины — всё в одном потоке.
import asyncio, httpx
async def fetch(client, url):
resp = await client.get(url, timeout=5)
return resp.status_code
async def main():
async with httpx.AsyncClient() as client:
print(await fetch(client, "https://example.ru"))
asyncio.run(main())
asyncio.run создаёт цикл событий и выполняет корутину. Вызов корутины без await ничего не делает — Python предупредит «coroutine was never awaited».
Одновременно, а не по очереди
a = await fetch(client, url1) # по очереди —
b = await fetch(client, url2) # выигрыша нет
a, b = await asyncio.gather(fetch(client, url1), fetch(client, url2))
gather запускает корутины конкурентно и возвращает результаты в том же порядке. С Python 3.11 есть TaskGroup:
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(fetch(client, url1))
t2 = tg.create_task(fetch(client, url2))
print(t1.result(), t2.result())
Разница — в ошибках. gather пробрасывает первую ошибку, но остальные задачи продолжают работать брошенными. TaskGroup отменяет остальные, ждёт их завершения и отдаёт ошибки группой ExceptionGroup — задач-сирот не остаётся.
Ограничить число одновременных запросов — asyncio.Semaphore.
Блокирующий вызов останавливает всё
Корутины переключаются только в точках await. Блокирующий вызов — time.sleep, requests.get, синхронный драйвер базы, тяжёлое вычисление — не отдаёт управление, и весь цикл событий стоит. Симптом: растут задержки всех запросов, хотя процессор почти не загружен.
Решения:
- асинхронные библиотеки — httpx, asyncpg, aiofiles;
- вынести блокирующий код в поток:
await asyncio.to_thread(func, arg); - тяжёлые вычисления — в пул процессов через
loop.run_in_executor.
Отладочный режим asyncio предупреждает о шагах дольше 100 мс. Во FastAPI обработчик def выполняется в пуле потоков, а async def — прямо в цикле, поэтому блокирующий код в async def особенно опасен.
Отмена и таймауты
task.cancel() бросает CancelledError внутрь корутины в ближайшей точке await. Таймаут:
try:
async with asyncio.timeout(3):
data = await fetch_slow()
except TimeoutError:
data = None
По истечении времени задача отменяется, наружу приходит TimeoutError.
CancelledError нельзя глотать: очистить ресурсы в finally или async with и пробросить дальше. С Python 3.8 он наследует BaseException, поэтому except Exception его не ловит — так и задумано. Синхронный код без await отменить нельзя: отмена сработает только на следующем await. asyncio.shield защищает внутреннюю операцию от отмены внешней.
Что запомнить
await— точка переключения; без него корутины не чередуются.- Одновременный запуск — gather или TaskGroup, а не
awaitподряд. - TaskGroup отменяет соседей при ошибке.
- Блокирующий вызов останавливает весь цикл —
to_threadили асинхронная библиотека. - CancelledError пробрасывают, а не глотают.