Как решать CPU-bound и IO-bound задачи в Python
Что проверяют
Понимаете ли вы, почему в Python выбор между потоками, процессами и asyncio зависит от характера нагрузки, и умеете ли определить этот характер на практике. Следом обычно спрашивают, что делать, если в одном сервисе есть и то и другое.
Как отвечать
Сначала диагностика: IO-bound задача большую часть времени ждёт сеть, диск или базу, CPU-bound считает. Отличить помогает сравнение time.process_time() с реальным временем или профиль py-spy.
- IO-bound, асинхронный стек:
asyncioс асинхронными клиентами (httpx,aiohttp,asyncpg). Задачи запускают черезasyncio.TaskGroup, число одновременных запросов ограничиваютasyncio.Semaphore; - IO-bound, синхронные библиотеки:
ThreadPoolExecutor. GIL отпускается на время системного вызова, поэтому потоки хорошо параллелят ожидание. Одиночный блокирующий вызов из корутины уносят вasyncio.to_thread; - CPU-bound:
ProcessPoolExecutorилиmultiprocessing.Pool. У каждого процесса свой интерпретатор и свой GIL. Цена: запуск процессов и сериализация аргументов и результатов через pickle, поэтому мелкие задачи объединяют параметромchunksize; - вынос расчёта из Python: NumPy и многие C-расширения отпускают GIL внутри тяжёлых операций, горячий цикл можно переписать на Cython или на Rust через PyO3;
- новые варианты: free-threaded сборка CPython (официально с 3.14) и появившийся в 3.14
InterpreterPoolExecutor. Оба пока требуют проверки совместимости библиотек.
Смешанный случай: сервис на FastAPI принимает запросы асинхронно, а тяжёлый расчёт отдаёт в пул процессов через loop.run_in_executor(process_pool, func, arg). Для долгих задач лучше очередь и отдельные воркеры.
Где спотыкаются
Запускают CPU-bound расчёт в потоках и удивляются, что стало медленнее из-за борьбы за GIL.
Пишут async def, но внутри вызывают requests или синхронный драйвер базы. Код выглядит асинхронным, а на деле обрабатывает запросы по одному.
Отправляют в пул процессов крошечные задачи или огромные объекты. Накладные расходы на pickle и передачу между процессами съедают весь выигрыш, а лямбды и локальные функции стандартный pickle вообще не сериализует.