> Что происходит при вызове синхронной функции из асинхронной ручки в 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 операции. Это создаёт эффект "заморозки" всего приложения.
Важно различать два случая:
-
Синхронная функция объявлена как
defв ручке FastAPI - FastAPI автоматически выполняет её в thread pool (черезrun_in_threadpool). Event loop остаётся свободным. -
Синхронная функция вызывается внутри
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.
Пример кода
PYTHONimport timeimport httpxfrom fastapi import FastAPIfrom starlette.concurrency import run_in_threadpoolapp = 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") # блокирует loopreturn 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, но не для других блокирующих операций.
> Похожие задачи по Python
Что является верхнеуровневым и низкоуровневым компонентом в системах на SOLID?
Как обойти блокировку при вызове синхронной функции в асинхронном коде в Python?
Какова иерархия процессов, потоков и asyncio в Python?
Что можно сделать для увеличения стабильности сетевых запросов?
> Похожие задачи по backend
Что является верхнеуровневым и низкоуровневым компонентом в системах на SOLID?
Как обойти блокировку при вызове синхронной функции в асинхронном коде в Python?
Какова иерархия процессов, потоков и asyncio в Python?
Что можно сделать для увеличения стабильности сетевых запросов?
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью