← Все вопросы

Запустил вычисления в 4 потока, а быстрее не стало. При чём тут GIL и когда брать multiprocessing?

Задан 3 месяца назад1.2к просмотров3 ответа
8

Считаю что-то тяжёлое в цикле (перемножаю числа, факторизую), решил ускорить через потоки — раскидал на 4 threading.Thread. По итогу время вообще не изменилось, а то и хуже стало. Читал про GIL и совсем запутался: в чём разница между процессом и потоком и почему потоки не помогли?

Когда вообще брать threading, а когда multiprocessing?

3 ответа

12
✓ Принятый ответ — помог автору

Коротко: ты уткнулся в 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.

4

Дополню: блок if __name__ == "__main__": для multiprocessing — не формальность, а необходимость на Windows и macOS (там процессы создаются через spawn и заново импортируют твой модуль; без guard получишь бесконечное порождение процессов).

И ещё: в свежих версиях CPython (3.13+) есть экспериментальный режим без GIL (free-threaded). Но пока это опция при сборке, для рабочих задач по-прежнему ориентируйся на правило «CPU → процессы, I/O → потоки».

2

Если не хочется руками возиться с пулом, удобный единый интерфейс — concurrent.futures: ThreadPoolExecutor для I/O и ProcessPoolExecutor для CPU, API одинаковый, переключаешься одной строкой. Часто хорошая стратегия: написать на ThreadPoolExecutor, замерить, и если упёрся в GIL — поменять на ProcessPoolExecutor.

Ваш ответ

Войдите, чтобы ответить на вопрос.
Поддержать проект