Multiprocessing: процессы для CPU-задач

Если потоки не ускоряют счёт из-за GIL — запустим несколько процессов, у каждого свой GIL.

multiprocessing — модуль Python, запускающий задачи в отдельных процессах, у каждого свой интерпретатор и свой GIL, что даёт настоящий параллелизм на нескольких ядрах.

GIL блокирует параллелизм потоков внутри одного процесса. Но он не мешает разным процессам! Каждый процесс — отдельный интерпретатор Python со своим GIL. Запустив 4 процесса на 4 ядрах, вы получите 4-кратное ускорение для CPU-задач.

Базовый пример

from multiprocessing import Pool

def heavy(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":
    with Pool(4) as p:                 # 4 процесса на 4 ядра
        results = p.map(heavy, [10**6, 10**6, 10**6, 10**6])
    print(sum(results))

Обратите внимание на if __name__ == "__main__" — без него на некоторых ОС запуск процессов уйдёт в бесконечную рекурсию.

Цена параллелизма процессов

За настоящий параллелизм платят. У процессов раздельная память, поэтому аргументы и результаты приходится сериализовать (pickle) и пересылать между процессами. Для мелких задач эти накладные расходы могут съесть весь выигрыш — процессы выгодны на по-настоящему тяжёлых вычислениях.

# смоделируем выигрыш: 4 задачи по 2 сек на 4 ядрах
task_time = 2.0
tasks = 4
cores = 4

sequential = task_time * tasks
parallel = task_time * (tasks / cores)   # все 4 разом

print("последовательно:", sequential, "сек")
print("4 процесса:", parallel, "сек")

Вывод:

последовательно: 8.0 сек
4 процесса: 2.0 сек

Когда что выбирать

НагрузкаИнструментПочему
Тяжёлый счёт (CPU-bound)multiprocessingобходит GIL, реальный параллелизм
Ожидание сети/диска (I/O-bound)threading / asyncioGIL отпускается, потоки дешевле процессов

Как работает под капотом

При старте процесса ОС создаёт отдельное адресное пространство (через fork или spawn). Данные между процессами идут через каналы (pipes) и очереди, и всё, что пересылается, должно поддерживать pickle. Поэтому, например, нельзя передать в процесс лямбду или открытый файловый дескриптор. Pool переиспользует фиксированное число процессов, раздавая им задачи, — это избавляет от затрат на постоянное создание новых.

Частые ошибки

  • Забыть if __name__ == "__main__". На Windows/macOS со spawn это приводит к рекурсивному запуску.
  • Гонять через процессы мелочёвку. Сериализация и пересылка съедят выигрыш — процессы для тяжёлых задач.
  • Ждать общую память «как у потоков». У процессов память раздельная; общий доступ требует особых средств (Value, shared_memory).

Итог

  • У каждого процесса свой GIL — отсюда настоящий параллелизм.
  • multiprocessing — выбор для CPU-bound задач.
  • Данные между процессами сериализуются (pickle), это стоит времени.
  • Для мелких задач накладные расходы могут перевесить выигрыш.
Проверьте себя
1. Почему multiprocessing даёт настоящий параллелизм, а threading — нет?
AПроцессы игнорируют вычисления
BУ каждого процесса свой интерпретатор и свой GIL, поэтому они считают одновременно
CПроцессы работают только на одном ядре
Dmultiprocessing отключает GIL во всей программе
2. Какова главная плата за параллелизм через процессы?
AПроцессы нельзя остановить
BДанные между процессами нужно сериализовать (pickle) и пересылать
CПроцессы не видят CPU
DОни требуют интернета
3. Для какой нагрузки multiprocessing подходит лучше всего?
AОжидание ответов сети
BТяжёлые CPU-bound вычисления
CПростой вывод текста
DЧтение одной строки из файла