Синхронность, асинхронность и event loop
Асинхронность — это способ обслуживать множество запросов одним потоком, переключаясь между ними в моменты ожидания ввода-вывода, а не блокируясь на каждом.
Главная мысль:
asyncне делает код «быстрее» в смысле вычислений. Он позволяет не простаивать, пока программа ждёт ответа от сети или диска, и за это время заняться другим запросом.
Чтобы понять, зачем FastAPI вообще асинхронный, нужно осознать, на что бэкенд тратит время. В типичном API процессор почти не загружен: основная задержка — это ожидание. Запрос пришёл, нужно сходить в базу данных и подождать ответа, дёрнуть внешний платёжный сервис и подождать, прочитать файл и подождать. Пока один запрос «висит» в ожидании базы, синхронный сервер с одним потоком стоит без дела. Асинхронный сервер в этот момент переключается на другой запрос.
Сердце этого механизма — event loop (цикл событий). Это диспетчер, который держит список задач. Когда задача доходит до операции ожидания и говорит await, она как бы поднимает руку: «я подожду ответа сети, разбуди меня, когда придёт». Event loop откладывает её и берёт следующую готовую задачу. Когда сеть ответит, задача снова станет готовой и продолжит с того же места. Один поток, но иллюзия параллелизма для тысяч соединений.
Слова async def объявляют корутину — функцию, которую можно приостанавливать. Вызов корутины не запускает её немедленно; он возвращает объект корутины, который нужно «прокрутить» через await или event loop. Внутри корутины await ставится перед другой корутиной и означает «приостанови меня здесь, пока та не завершится».
Как работает под капотом
Смоделируем кооперативную многозадачность без всякого FastAPI, на чистом stdlib. Две «задачи» по очереди уступают управление через генераторы — ровно так концептуально устроен event loop:
def task(name, steps):
for i in range(steps):
print(f"{name}: шаг {i} (ждём ввод-вывод...)")
yield # точка, где задача уступает управление (аналог await)
print(f"{name}: готово")
# простейший планировщик-«event loop»
tasks = [task("запрос-A", 3), task("запрос-B", 2)]
while tasks:
still_running = []
for t in tasks:
try:
next(t) # продвигаем задачу на один шаг
still_running.append(t)
except StopIteration:
pass # задача завершилась
tasks = still_running
Попробуй сам ▶ Обрати внимание: шаги A и B чередуются, хотя поток один. Так же event loop чередует обработку запросов в моменты await.
В реальном FastAPI это выглядит так. Когда обработчик объявлен async def и внутри делает await db.fetch(...), event loop на время ожидания базы свободен для других запросов:
@app.get("/items/{item_id}")
async def read_item(item_id: int):
item = await db.fetch_item(item_id) # тут поток освобождается
return item
Частые ошибки
Самая частая ошибка — поставить async def, но внутри вызвать блокирующую функцию: обычный синхронный запрос к базе через psycopg2, time.sleep(), тяжёлый цикл по числам. Такой вызов не уступает управление event loop, и весь сервер замирает на это время, обслуживая всех остальных хуже, чем синхронный. Правило: внутри async def всё, что ждёт, должно ждать через await и асинхронные библиотеки.
Вторая ошибка — считать, что асинхронность ускоряет вычисления. Перемножение матриц или хеширование пароля — это работа процессора, а не ожидание; async тут не помогает и даже мешает, такие задачи выносят в отдельные потоки или процессы.
Best practices
- Используйте
async def, когда внутри есть настоящиеawaitк асинхронным библиотекам (БД, HTTP-клиенты, очереди). - Никогда не вызывайте блокирующий код напрямую в корутине — оборачивайте через
run_in_threadpoolили используйте sync-обработчик. - CPU-тяжёлые задачи выносите в фоновые воркеры (Celery, процессы), а не в event loop.
Конкурентность и параллелизм — не одно и то же
Эти слова часто путают, а различие принципиальное. Конкурентность (concurrency) — это умение управляться со многими задачами, чередуя их; параллелизм (parallelism) — это одновременное выполнение на нескольких ядрах. Async в Python даёт именно конкурентность: один поток, одно ядро, но иллюзия одновременности за счёт переключения в точках ожидания. Это идеально для задач, ограниченных вводом-выводом (I/O-bound), где время уходит на ожидание сети и диска. Для задач, ограниченных процессором (CPU-bound) — шифрование, обработка изображений, числодробление — нужен настоящий параллелизм через процессы, потому что вычисления не уступают управление сами по себе, и event loop тут бессилен. Запомните дихотомию: I/O-bound лечится асинхронностью, CPU-bound — процессами. Большинство веб-API именно I/O-bound, поэтому ставка FastAPI на async оправдана.
Итог: async — это про ожидание, а не про скорость вычислений. Event loop чередует задачи в точках await. В следующем уроке посмотрим, как FastAPI обращается с обычным def.