Запустил вычисления в 4 потока, а быстрее не стало. При чём тут GIL и когда брать multiprocessing?
Считаю что-то тяжёлое в цикле (перемножаю числа, факторизую), решил ускорить через потоки — раскидал на 4 threading.Thread. По итогу время вообще не изменилось, а то и хуже стало. Читал про GIL и совсем запутался: в чём разница между процессом и потоком и почему потоки не помогли?
Когда вообще брать threading, а когда multiprocessing?
3 ответа
Коротко: ты уткнулся в GIL. GIL (Global Interpreter Lock) — это глобальный замок интерпретатора CPython, который позволяет выполнять Python-байткод только одному потоку в каждый момент времени. То есть твои 4 потока не считают параллельно на 4 ядрах — они по очереди передают друг другу один и тот же GIL. Для чисто вычислительной (CPU-bound) задачи это не ускорение, а накладные расходы на переключение, отсюда и «стало хуже».
Разница процесс vs поток:
- Потоки живут внутри одного процесса и делят общую память. Создаются дёшево, но в CPython упираются в GIL на CPU-задачах.
- Процессы — это отдельные интерпретаторы Python со своей памятью и СВОИМ GIL у каждого. Поэтому они реально считают параллельно на разных ядрах. Платишь за это ценой: дороже стартуют, данные между ними нужно сериализовать (pickle).
Правило выбора:
- I/O-bound (сеть, диск, ожидание ответа) →
threadingили async. Пока поток ждёт ответ, GIL отпускается, и другие потоки работают. Тут потоки реально ускоряют. - CPU-bound (числодробилка) →
multiprocessing. Обходим GIL, грузим все ядра.
Твой пример на процессах:
from multiprocessing import Pool
def heavy(n):
return sum(i * i for i in range(n))
if __name__ == "__main__": # на Windows/macOS обязателен этот guard
with Pool(4) as pool:
results = pool.map(heavy, [10_000_000] * 4)
print(results)
Вот тут на 4 ядрах ты увидишь реальное ускорение, потому что у каждого процесса свой GIL.
Дополню: блок if __name__ == "__main__": для multiprocessing — не формальность, а необходимость на Windows и macOS (там процессы создаются через spawn и заново импортируют твой модуль; без guard получишь бесконечное порождение процессов).
И ещё: в свежих версиях CPython (3.13+) есть экспериментальный режим без GIL (free-threaded). Но пока это опция при сборке, для рабочих задач по-прежнему ориентируйся на правило «CPU → процессы, I/O → потоки».
Если не хочется руками возиться с пулом, удобный единый интерфейс — concurrent.futures: ThreadPoolExecutor для I/O и ProcessPoolExecutor для CPU, API одинаковый, переключаешься одной строкой. Часто хорошая стратегия: написать на ThreadPoolExecutor, замерить, и если упёрся в GIL — поменять на ProcessPoolExecutor.