Какие способы выполнения синхронных задач существуют в FastAPI?
Что проверяют
Понимаете ли вы, чем def отличается от async def в обработчике и что происходит с блокирующим кодом внутри цикла событий. Вопрос отсеивает тех, кто пишет async def везде по привычке. Следом спрашивают, что будет, если в асинхронном обработчике вызвать time.sleep(5) или синхронный драйвер базы.
Как отвечать
Главное различие FastAPI задаёт сам:
- обработчик через
def: FastAPI отправляет его в пул потоков (anyio.to_thread), цикл событий не блокируется. По умолчанию пул ограничен 40 потоками, и на синхронных походах в базу вы упрётесь именно в этот лимит; - обработчик через
async def: выполняется прямо в цикле. Любой блокирующий вызов внутри останавливает весь воркер, а не один запрос; - точечный вынос: в асинхронном коде оборачивайте блокирующий вызов в
await asyncio.to_thread(...),anyio.to_thread.run_syncилиstarlette.concurrency.run_in_threadpool; - CPU-bound: потоки тут не спасают из-за GIL, нужен
ProcessPoolExecutorчерезloop.run_in_executorлибо вынос в отдельный воркер; - BackgroundTasks: выполняется после отправки ответа, но в том же процессе. Годится для письма или записи в лог, не для минутной работы: перезапуск контейнера такие задачи теряет, а ошибку клиент уже не увидит;
- очередь: Celery, ARQ, Dramatiq или taskiq, когда задача долгая, требует повторов и наблюдаемости.
Добавьте про зависимости: синхронный Depends тоже уходит в пул потоков. И про горизонтальное масштабирование процессами через --workers у uvicorn или gunicorn.
Где спотыкаются
Пишут async def и внутри requests.get или синхронный psycopg. Под нагрузкой сервис встаёт целиком, а на одном запросе в тестах всё выглядит нормально.
Запускают тяжёлую работу через BackgroundTasks и считают, что получили очередь. Ни повторов, ни персистентности, ни контроля за результатом там нет.
Создают собственный ThreadPoolExecutor и тут же ждут future.result() в синхронном обработчике: поток из пула стоит и ждёт другой поток, выигрыша ноль.