Имеет ли смысл ускорять тяжелую процессорную задачу с помощью асинхронного кода
Что проверяют
Понимаете ли вы, что asyncio ускоряет ожидание, а не вычисления. Следом обычно спрашивают про GIL и про то, что делать, если тяжёлый расчёт нужно выполнить внутри асинхронного веб-сервиса.
Как отвечать
Короткий ответ: нет. asyncio работает в одном потоке, event loop переключает корутины только в точках await. Вычислительная корутина до await не доходит и держит цикл целиком: задача быстрее не станет, а все остальные запросы сервиса встанут в очередь. В режиме отладки asyncio предупреждает о шагах дольше 100 мс, так такие места и ловят.
Что делать вместо этого:
- процессы:
ProcessPoolExecutorизconcurrent.futuresилиmultiprocessing. У каждого процесса свой интерпретатор и свой GIL, так нагружаются все ядра. Цена: запуск процессов и сериализация аргументов через pickle, поэтому мелкие задачи выгоднее отправлять пачками; - связка с asyncio:
await loop.run_in_executor(pool, func, data)уносит расчёт в пул процессов, а цикл продолжает обслуживать запросы. Асинхронность тут не ускоряет задачу, а сохраняет отзывчивость сервиса; - потоки:
ThreadPoolExecutorиasyncio.to_threadпомогают, только если код отпускает GIL, как NumPy,hashlibна больших буферах или C-расширения; - нативный код: векторизация NumPy, Numba, Cython, расширения на Rust через PyO3 часто дают больше любого распараллеливания;
- фоновые очереди: долгий расчёт выносят из запроса в Celery или аналог.
Отдельно упомяните free-threaded сборку CPython без GIL: экспериментально она появилась в 3.13 и официально поддерживается с 3.14, но остаётся отдельной сборкой, и не все C-расширения с ней совместимы.
Где спотыкаются
Советуют многопоточность вместо асинхронности для вычислений. Для чистого Python в обычной сборке это то же ограничение GIL, выигрыша не будет.
Думают, что concurrent.futures всегда означает процессы. Процессы даёт только ProcessPoolExecutor, пул потоков оставляет всё под GIL.
Вызывают тяжёлую функцию прямо в обработчике FastAPI, объявленном через async def. Такой обработчик блокирует event loop всего воркера, и остальные запросы ждут.