Бортовой журнал
Бортовой журнал

Современные программы все чаще требуют высокой производительности для обработки больших объемов данных, работы с изображениями, моделирования или машинного обучения. Последовательное выполнение таких задач может занимать слишком много времени, особенно если ресурсы процессора используются не полностью.

Один из способов ускорить выполнение — задействовать несколько ядер процессора одновременно. В Python для этого предназначен модуль multiprocessing.

Что такое multiprocessing в Python

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

Зачем нужен модуль multiprocessing

Модуль нужен тогда, когда программа должна выполнять несколько ресурсоемких задач быстрее. Например:

  • Ускорение вычислений. Если есть большие массивы данных для обработки, их можно разделить между процессами и сократить общее время работы.
  • Параллельная обработка задач. Можно одновременно запускать конвертацию файлов, работу с базой данных и другие тяжелые операции.
  • Эмуляция работы серверов. Модуль позволяет создавать пулы процессов, которые распределяют входящие задачи между собой — это похоже на то, как устроены веб-серверы.
  • Обход ограничений GIL. Если в Python многопоточность не может задействовать несколько ядер одновременно, модуль multiprocessing запускает отдельные процессы и тем самым параллельно использует ресурсы процессора.

Разница между многопоточностью и многопроцессностью

Критерий Многопоточность Многопроцессность
Запуск Все потоки внутри одного процесса Каждый процесс — отдельный Python-интерпретатор
Память Общая память для всех потоков У каждого процесса своя память
GIL Ограничивает параллельность Не влияет, процессы работают независимо
Лучше подходит для Задачи с большим I/O CPU-нагрузка, тяжелые вычисления
Производительность Быстрый запуск, но ограниченный прирост Полный доступ ко всем ядрам, выше ускорение
Минусы Упирается в GIL, неэффективно для CPU Более высокая нагрузка на память и ресурсы

В Python часто путают многопоточность (threading) и многопроцессность (multiprocessing). На первый взгляд они похожи: и там, и там можно выполнять несколько задач одновременно. Но все не так просто.

  • Многопоточность — это когда в одном процессе запускаются несколько потоков, которые делят между собой общую память. В Python ее эффективность сильно ограничена из-за GIL, поэтому потоки больше подходят для задач, где нужно много ждать (например, работа с сетью или вводом-выводом).
  • Многопроцессность — это когда создаются несколько отдельных процессов. Каждый процесс — независимая копия Python, которая работает на своем ядре процессора. Она лучше для тяжелых вычислений, но дороже по ресурсам.

Параллельные вычисления в Python: как это работает

Когда мы пишем обычный Python-код, он выполняется последовательно: одна строчка за другой. Такой подход прост и понятен, но имеет очевидный минус — если задача занимает много времени, программа будет простаивать, пока она не завершится. Здесь на помощь приходят параллельные вычисления.

Смысл параллельных вычислений в том, чтобы разделить задачу на несколько частей и запустить их одновременно. Это можно делать двумя основными способами: через потоки и через процессы.

Потоки — это ветки внутри одного процесса. У них общая память, поэтому данные можно легко передавать между потоками. Но в Python есть ограничение — Global Interpreter Lock (GIL). Из-за него одновременно работает только один поток, а остальные ждут своей очереди. Получается, что многопоточность в Python эффективна в первую очередь для задач, где мы много ждем: работа с сетью, чтение файлов или ввод-вывод.

Процессы, наоборот, запускают независимые копии интерпретатора Python. Каждый процесс может выполняться на своем ядре процессора, поэтому ограничения GIL здесь нет. Именно многопроцессность позволяет по максимуму загружать процессор и ускорять тяжелые вычисления — например, обработку изображений, видео или больших массивов данных. Минус в том, что процессы более тяжелые и требуют больше памяти, а обмен данными между ними сложнее, чем у потоков.

Чтобы упростить жизнь разработчику, в Python есть несколько модулей:

  • multiprocessing — низкоуровневый инструмент для работы с процессами. Позволяет создавать процессы вручную, использовать очереди и пайпы для обмена данными, а также объединять задачи в пул.
  • threading — классическая работа с потоками. Удобно использовать для сетевых запросов, фоновых задач или работы с файлами.
  • concurrent.futures предлагает единый интерфейс к пулам потоков и процессов. С его помощью можно легко распределить задачи по исполнителям и собрать результаты, не углубляясь в детали синхронизации или передачи данных.

Когда стоит использовать multiprocessing вместо threading

multiprocessing применяется в ситуациях, где требуется максимальная загрузка процессора и распределение вычислений по всем доступным ядрам. Он оправдан, если задача связана с интенсивными вычислениями и может быть разделена на независимые части.

Основные случаи, когда целесообразно использовать multiprocessing:

  • Вычислительно сложные задачи. Обработка изображений и видео, математическое моделирование, машинное обучение, анализ больших массивов данных.
  • Длительные операции. Алгоритмы, которым нужно значительное количество времени на выполнение и которые не зависят от ввода-вывода.
  • Необходимость масштабирования. Возможность распределить задачи по нескольким ядрам для ускорения обработки.
  • Требования к устойчивости. У отдельных процессов есть собственное пространство памяти, поэтому сбой в одном из них не влияет на остальные.
  • Изоляция состояния. Когда важно, чтобы каждая задача выполнялась в независимой среде, без общего доступа к данным.

Таким образом, модуль multiprocessing применяется прежде всего для задач, которые активно нагружают процессор и могут быть параллельно разделены на несколько самостоятельных вычислительных блоков.

Основы модуля multiprocessing

Когда вы запускаете программу на Python, она работает в одном процессе. Это значит, что весь код выполняется по порядку: сначала одна задача, потом другая.

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

Он позволяет создать новый процесс, то есть отдельную мини-программу, которая выполняет ваш код параллельно с основной программой.

Пример 1. Запуск простого процесса:

from multiprocessing import Process
import time
# Функция, которая будет выполняться в процессе
def worker():
print("Процесс начал работу")
time.sleep(2)  # имитация долгой задачи
print("Процесс завершил работу")
if __name__ == "__main__":
# создаем процесс
p = Process(target=worker)
# запускаем процесс
p.start()
# ждем его завершения
p.join()
print("Главная программа завершена")
Изображение 1

В этом примере создается отдельный процесс, которому назначается функция worker. Метод start() запускает выполнение этой функции параллельно с основной программой, а вызов join() заставляет главную программу дождаться завершения процесса, прежде чем продолжить выполнение.

В итоге сначала отрабатывает функция в отдельном процессе, а после ее завершения выводится сообщение из главного кода:

Изображение 2

Пример 2. Несколько процессов с аргументами:

from multiprocessing import Process
import time
# Функция, которая считает факториал числа
def factorial(n):
result = 1
for i in range(1, n + 1):
result *= i
time.sleep(0.1)  # добавим паузу, чтобы увидеть параллельность
print(f"Факториал числа {n} равен {result}")
if __name__ == "__main__":
numbers = [5, 6, 7]
processes = []
# создаем процесс для каждого числа
for num in numbers:
p = Process(target=factorial, args=(num,))
processes.append(p)
p.start()
# дожидаемся завершения всех процессов
for p in processes:
p.join()
print("Все процессы завершены")
Изображение 3

Здесь сразу три процесса считают факториалы разных чисел. Они работают одновременно, поэтому программа завершается быстрее, чем если бы все вычисления шли последовательно.

Вывод:

Изображение 4

Продвинутые функции multiprocessing

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

Queue, Pipe и обмен данными между процессами

Когда мы запускаем несколько процессов, каждый из них работает в своей области памяти. Это значит, что они не видят напрямую переменные друг друга.

Чтобы процессы могли общаться между собой и передавать данные, в модуле multiprocessing предусмотрены специальные инструменты: очереди (Queue) и каналы (Pipe).

Queue работает по принципу FIFO — «первым пришел — первым ушел». Один процесс может положить данные в очередь, а другой — забрать их:

from multiprocessing import Process, Queue
def producer(q):
for i in range(5):
q.put(i)  # кладем данные
q.put(None)   # специальный маркер окончания
def consumer(q):
while True:
item = q.get()  # достаем данные
if item is None:  # если встретили маркер — завершаем
break
print("Получено:", item)
if __name__ == "__main__":
queue = Queue()
p1 = Process(target=producer, args=(queue,))
p2 = Process(target=consumer, args=(queue,))
p1.start()
p2.start()
p1.join()
p2.join()
Изображение 5

Вывод:

Изображение 6

Pipe — это двусторонний канал связи между двумя процессами. Работает быстрее, чем очередь, но подходит только для связи «один к одному». У канала есть два конца, и каждый из них может отправлять данные с помощью метода send() и принимать их через recv().

Например, один процесс отправляет данные, другой их получает:

from multiprocessing import Process, Pipe
def worker(conn):
conn.send(["данные", 123, True])  # отправляем список
conn.close()
if __name__ == "__main__":
parent_conn, child_conn = Pipe()
p = Process(target=worker, args=(child_conn,))
p.start()
msg = parent_conn.recv()  # получаем данные
print("Сообщение от процесса:", msg)
p.join()
Изображение 7

Вывод:

Изображение 8

Pool: параллельное выполнение задач из пула

Если нужно обработать большой набор однотипных данных — например, вычислить функцию для тысячи чисел или обработать сотни файлов, — то неудобно вручную запускать процессы. Для таких случаев в модуле multiprocessing есть класс Pool, который управляет группой рабочих процессов (воркеров) и сам распределяет между ними задачи.

Пример:

from multiprocessing import Pool
import time
# список задач: имя задачи и время ее выполнения
tasks = [("Task-1", 4), ("Task-2", 2), ("Task-3", 3), ("Task-4", 1)]
def worker(task_data):
name, delay = task_data
print(f"{name} запущена, время выполнения {delay} сек.")
time.sleep(delay)  # имитация «тяжелой работы»
print(f"{name} завершена.")
return f"{name} -> готово"
def run_pool():
# создаем пул из 2 процессов
with Pool(2) as pool:
results = pool.map(worker, tasks)
print("\nВсе результаты:")
for r in results:
print(r)
if __name__ == "__main__":
run_pool()
Изображение 9

В этом примере создается пул из двух процессов, и в него передается список задач. Каждый процесс берет задачу из списка и выполняет ее.

Сначала запускаются Task-1 и Task-2, так как пул ограничен двумя воркерами. Когда Task-2 заканчивает работу, ее место занимает Task-3. После завершения Task-1 второй процесс берет Task-4. Таким образом, в каждый момент времени выполняется не более двух задач, а оставшиеся ждут своей очереди:

Изображение 10

Lock и управление ресурсами

При параллельном выполнении кода несколько процессов могут одновременно обращаться к одному и тому же ресурсу: файлу, разделяемой структуре данных, порту, устройству. Из-за этого данные могут повредиться или результат работы становится непредсказуемым. Чтобы этого избежать, в модуле multiprocessing используется механизм блокировок — класс Lock.

Lock позволяет ограничить доступ к ресурсу так, чтобы в каждый момент времени его использовал только один процесс:

  • Метод acquire() блокирует ресурс. Пока он занят, другие процессы ждут.
  • Метод release() освобождает ресурс, после чего им может воспользоваться следующий процесс.

В Python удобнее всего использовать конструкцию with lock:, которая автоматически захватывает и освобождает ресурс:

from multiprocessing import Process, Lock
import time
def write_to_file(lock, filename, text):
with lock:  # блокировка на время записи
with open(filename, "a", encoding="utf-8") as f:
f.write(text + "\n")
print(f"Записано: {text}")
if __name__ == "__main__":
lock = Lock()
filename = "output.txt"
# создаем несколько процессов для записи
processes = [
Process(target=write_to_file, args=(lock, filename, f"Строка {i}"))
for i in range(5)
]
for p in processes:
p.start()
for p in processes:
p.join()
Изображение 11

Итак, каждый процесс пытается записать строку в один и тот же файл. Благодаря Lock запись происходит по очереди: сначала один процесс, затем другой:

Изображение 12

Ошибки и сложности при работе с multiprocessing

При переходе к параллельному исполнению часто возникают типичные проблемы, которые связаны с особенностями работы отдельных процессов и механизмов многопроцессности:

  • Незавершенные процессы и утечки ресурсов. Если забыть вызвать join(), дочерние процессы могут остаться висеть. Повторный запуск создает множество фоновых интерпретаторов и постепенно исчерпывает память.
  • Взаимные блокировки. Неправильное управление очередями или замками приводит к ситуации, когда процессы навсегда ждут освобождения ресурса друг от друга. Всегда устанавливайте четкие протоколы завершения и минимизируйте критические секции под Lock.
  • Накладные расходы на создание и обмен данными. Запуск нового процесса и сериализация объектов занимают время и ресурсы. Для множества мелких задач выигрыша не будет — накладные расходы могут превышать ускорение.
  • Сложность отладки. Стандартные отладчики не всегда корректно работают с несколькими процессами. Логирование через общий файл или использование отдельных выводов в консоль помогут понять хронологию событий.

Заключение

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

При работе с параллельными вычислениями важно соблюдать осторожность: процессы нужно правильно синхронизировать, следить за их завершением и учитывать дополнительные затраты на управление ими. Но если всё настроено грамотно, использование multiprocessing способно заметно ускорить выполнение и сделать Python-программы гораздо эффективнее при обработке больших данных и ресурсоемких задач.