> Что происходит при вызове синхронной функции из асинхронной ручки в FastAPI? (Python)

Уровень: senior · Роль: backend · Язык: Python · Категория: Технические вопросы

Компании: HeadHunter

Стек: Python

> Пример ответа

Короткий ответ

При вызове синхронной функции из асинхронной ручки FastAPI она выполняется в том же event loop, блокируя его на время выполнения. Это означает, что все остальные запросы и корутины в приложении будут ждать завершения этой функции. FastAPI не определяет автоматически синхронные вызовы внутри асинхронных функций - он обрабатывает только синхронные функции, объявленные как def, через thread pool. Для блокирующих операций внутри async def нужно использовать run_in_executor или anyio.to_thread.

Подробное объяснение

FastAPI построен на Starlette и работает поверх asyncio. Когда вы объявляете ручку как async def, FastAPI запускает её в event loop. Если внутри этой ручки вызывается синхронная функция (например, time.sleep, requests.get, тяжёлые вычисления), она выполняется прямо в потоке event loop.

Event loop - это однопоточный планировщик. Пока синхронная функция выполняется, loop не может обрабатывать другие задачи: входящие запросы, таймеры, I/O операции. Это создаёт эффект "заморозки" всего приложения.

Важно различать два случая:

  1. Синхронная функция объявлена как def в ручке FastAPI - FastAPI автоматически выполняет её в thread pool (через run_in_threadpool). Event loop остаётся свободным.

  2. Синхронная функция вызывается внутри async def ручки - она выполняется в event loop. FastAPI не вмешивается.

Для второго случая правильный подход - использовать await run_in_threadpool(func, ...) (из Starlette) или await anyio.to_thread.run_sync(func). Это выгружает выполнение в отдельный поток, не блокируя loop.

Ещё нюанс: если синхронная функция выполняет I/O (например, чтение файла), она всё равно блокирует loop, потому что системный вызов read() блокирует поток. Только асинхронные версии I/O (через asyncio.open, aiofiles) не блокируют.

На практике

В реальных проектах это частая причина деградации производительности. Например, если в async def ручке вызывается requests.get() - это блокирует весь сервер на время сетевого запроса. При высокой нагрузке это приводит к таймаутам и падению throughput.

Практические рекомендации:

  • Для блокирующих операций (сеть, диск, CPU) используйте def ручки, а не async def.
  • Если ручка уже async def, оборачивайте блокирующие вызовы в await run_in_threadpool(...).
  • Для CPU-bound задач лучше использовать отдельный процесс (multiprocessing), а не thread pool - из-за GIL.
  • Для I/O-bound задач предпочитайте асинхронные библиотеки: httpx.AsyncClient вместо requests, aiofiles вместо open.

Пример кода

PYTHON
import time
import httpx
from fastapi import FastAPI
from starlette.concurrency import run_in_threadpool
app = FastAPI()
# Плохо: блокирует event loop
@app.get("/bad")
async def bad_endpoint():
time.sleep(2) # блокирует весь сервер
return {"status": "ok"}
# Плохо: блокирует event loop
@app.get("/bad-http")
async def bad_http():
response = httpx.get("https://api.example.com") # блокирует loop
return response.json()
# Хорошо: FastAPI сам запустит в thread pool
@app.get("/good")
def good_endpoint():
time.sleep(2)
return {"status": "ok"}
# Хорошо: явно выгружаем в thread pool
@app.get("/good-http")
async def good_http():
response = await run_in_threadpool(
httpx.get, "https://api.example.com"
)
return response.json()
# Лучше: полностью асинхронный подход
@app.get("/best-http")
async def best_http():
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com")
return response.json()

Как отвечать на собеседовании

Начните с чёткого ответа: синхронная функция внутри async def блокирует event loop. Затем объясните механизм: FastAPI различает def и async def ручки, но не анализирует содержимое асинхронных функций. Приведите пример с time.sleep или requests.get.

Покажите понимание thread pool: для def ручек FastAPI использует run_in_threadpool, поэтому они не блокируют loop. Для async def - вы сами отвечаете за неблокирующее выполнение.

Упомяните разницу между I/O-bound и CPU-bound задачами. Для I/O - thread pool или асинхронные библиотеки. Для CPU - multiprocessing.

Если спросят про asyncio.to_thread - это аналог run_in_threadpool из стандартной библиотеки, но run_in_threadpool использует собственный pool Starlette, что лучше для интеграции с FastAPI.

Что проверяет интервьюер

Интервьюер проверяет:

  • Понимание модели asyncio и event loop.
  • Знание того, как FastAPI обрабатывает синхронные и асинхронные ручки.
  • Умение выявлять блокирующие операции в асинхронном коде.
  • Практические навыки: знание run_in_threadpool, anyio.to_thread, асинхронных HTTP-клиентов.
  • Понимание trade-off между thread pool и multiprocessing.

Типичные ошибки

  • Утверждение, что FastAPI автоматически обрабатывает синхронные вызовы внутри async def - это не так.
  • Использование requests внутри async def без обёртки в thread pool.
  • Путаница между def ручками (которые FastAPI запускает в thread pool) и async def ручками.
  • Игнорирование GIL при обсуждении CPU-bound задач - thread pool не ускорит вычисления.
  • Предложение использовать asyncio.sleep вместо time.sleep как универсальное решение - это работает только для sleep, но не для других блокирующих операций.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью