Как параллелить выполнение функции в Python
Что проверяют
Знаете ли вы про GIL и умеете ли выбрать инструмент под тип нагрузки: ожидание сети и диска или счёт на процессоре. Следом обычно просят распараллелить обработку большого DataFrame или объяснить, почему потоки не ускорили расчёт.
Как отвечать
Сначала классифицируйте задачу, потом выбирайте механизм:
- I/O-bound (запросы к API, чтение из S3, запросы к базе):
ThreadPoolExecutorизconcurrent.futuresилиasyncio. Пока поток ждёт ответа, GIL отпущен, и остальные работают; - CPU-bound на чистом Python:
ProcessPoolExecutorилиmultiprocessing.Pool. Каждый процесс получает свой интерпретатор и свой GIL, аргументы и результаты передаются черезpickle; - NumPy, pandas, scikit-learn: многие операции выполняются в C и отпускают GIL, а BLAS сам использует несколько ядер. Часто достаточно векторизации, а у моделей есть параметр
n_jobs, который работает черезjoblib; - данные не помещаются на одну машину: Dask, Ray или Spark.
Полезные детали для ProcessPoolExecutor: executor.map с параметром chunksize снижает накладные расходы на мелких задачах, as_completed отдаёт результаты по мере готовности. На macOS и Windows процессы стартуют методом spawn, поэтому код запуска обязан стоять под if __name__ == "__main__":, иначе дочерние процессы начнут выполнять модуль заново.
Хорошо упомянуть free-threaded сборку CPython: начиная с 3.13 интерпретатор можно собрать без GIL, в 3.14 такая сборка официально поддерживается, но она не по умолчанию, и не все C-расширения с ней совместимы.
Где спотыкаются
Берут threading для тяжёлого счёта на Python и удивляются, что стало медленнее. Из-за GIL потоки выполняют байткод по очереди, а переключение добавляет расходы.
Передают в каждый процесс огромный DataFrame целиком. Сериализация и копирование съедают весь выигрыш, лучше передавать путь к файлу или границы диапазона.
Создают вложенный параллелизм: пул из 16 процессов, внутри каждого модель с n_jobs=-1 и многопоточный BLAS. Потоков становится в разы больше, чем ядер, и всё тормозит. Лечится ограничением OMP_NUM_THREADS и одним уровнем параллельности.