Короткий ответ
Решение зависит от того, можно ли выразить вычисления через существующие библиотеки, уже освобождающие GIL во внутренних циклах (NumPy, pandas и т.п.) — тогда многопоточность может дать реальный параллелизм почти бесплатно; если такой библиотеки нет, выбор между multiprocessing и собственным C-расширением определяется объёмом вычислений, частотой изменений этого кода и экспертизой команды.
Как это работает подробнее
GIL ограничивает параллельное выполнение именно байт-кода CPython; библиотеки, реализованные в C/Rust, могут явно освобождать GIL на время выполнения своих внутренних циклов — в этом случае потоки Python действительно получают параллелизм на таких участках. Multiprocessing даёт параллелизм за счёт отдельных процессов с собственным GIL, независимо от того, чистый это Python-код или нет, но за счёт издержек на передачу данных между процессами.
- Компромисс. Многопоточность с GIL-освобождающими библиотеками даёт лучшее сочетание простоты и производительности, но ограничена тем, что реально покрыто такими библиотеками; multiprocessing универсален, но создаёт издержки на сериализацию и использует больше памяти; собственное C-расширение даёт максимальный контроль, но увеличивает стоимость разработки и усложняет поставку — выбор зависит от критичности этой части для общей производительности и доступного бюджета разработки.
Пример кода
import numpy as np
def pure_python_sum(n):
return sum(i * i for i in range(n)) # чистый Python - GIL не освобождается, потоки не ускорят
def numpy_sum(n):
arr = np.arange(n, dtype=np.int64)
return int(np.sum(arr * arr)) # внутренний цикл NumPy может освобождать GIL
# Сравнение времени выполнения 4 потоков с pure_python_sum против 4 потоков с numpy_sum
# на многоядерной машине покажет, что numpy-вариант масштабируется заметно лучше с числом потоковЧто использовать на практике
- Практика. Сначала стоит проверить, не покрывает ли задачу уже существующая библиотека с освобождением GIL — это обычно наименее затратный путь. Если вычисления специфичны, multiprocessing — следующий по сложности вариант, подходящий, когда объём данных на передачу невелик относительно объёма вычислений. Собственное C-расширение (или Cython/Rust через PyO3) оправдано, когда вычисления — узкое горлышко всей системы.
Подводные камни
- Риски. Пишут «параллельный» код на чистых потоках для CPU-bound вычислений без проверки, освобождает ли используемая библиотека GIL, и получают разочаровывающий результат; берутся за написание C-расширения без предварительной оценки, что уже существующая библиотека или просто multiprocessing решают задачу с приемлемой производительностью при значительно меньших усилиях.