Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Multiprocessing: параллельные вычисления

Multiprocessing: параллельные вычисления в Python

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

Прямой ответ

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

from concurrent.futures import ProcessPoolExecutor

def compute(chunk):
    return sum(x * x for x in chunk)

with ProcessPoolExecutor(max_workers=4) as pool:
    chunks = [range(i, i + 250_000) for i in range(0, 1_000_000, 250_000)]
    results = list(pool.map(compute, chunks))
Аспект Поведение
Память Каждый процесс имеет собственное адресное пространство
Передача данных Аргументы и результаты сериализуются
Ускорение Достижимо для вычислительных задач

Аргументы и результаты передаются через сериализацию, поэтому объект, который нельзя сериализовать, не передаётся в процесс. О выборе модели — в статье про выбор между потоками и процессами.

Сериализация данных и объём обмена

Передача данных между процессами определяет накладные расходы, поэтому два аспекта влияют на фактическое ускорение.

Сериализация аргументов. Объект преобразуется в последовательность байтов для передачи, поэтому объём сериализации влияет на время. Большой объём данных передаётся дольше, чем выполняется вычисление.

Сбор результатов. Результат каждого процесса сериализуется и передаётся в основной процесс, поэтому объём результата также влияет на время.

Практическое следствие: данные делятся на части так, чтобы объём передачи был мал относительно объёма вычислений.

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

Три признака того, что многопроцессность не даёт ускорения.

Малый объём вычислений. Накладные расходы на создание процесса и передачу данных превышают время вычислений.

Большой объём передаваемых данных. Сериализация занимает больше времени, чем вычисление, поэтому ускорение теряется.

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

О влиянии блокировки интерпретатора — в статье про GIL; о пулах задач — в статье про concurrent.futures.

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

Общее состояние и способы его передачи

Процессы не разделяют адресное пространство, поэтому общее состояние передаётся явно.

Передача через аргументы. Данные передаются при отправке задачи, поэтому состояние доступно в процессе с начала выполнения. Изменение состояния в процессе не отражается в основном процессе.

Возврат результата. Результат передаётся обратно через сериализацию, поэтому объём результата влияет на время. Агрегированное значение передаётся быстрее полного набора.

Разделяемая память. Для больших наборов данных используется разделяемая память, поэтому сериализация не выполняется. Доступ требует синхронизации.

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

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

Завершение процессов и обработка сбоев

Процесс, завершившийся с ошибкой, не влияет на остальные, поэтому сбой изолируется.

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

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

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

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

Практика: настройка параллельных вычислений

1. Убедитесь, что задача вычислительная. Задача с ожиданием ввода-вывода выполняется в потоках без накладных расходов процессов.

2. Разделите данные на части. Объём передачи должен быть мал относительно объёма вычислений.

3. Используйте пул процессов. Создание процесса занимает время, поэтому пул переиспользует процессы для нескольких задач.

4. Проверяйте сериализуемость аргументов. Объект с ссылкой на несохраняемый ресурс не передаётся в процесс.

5. Определяйте функцию на уровне модуля. Функция, передаваемая в процесс, должна быть доступна по имени в модуле.

6. Выбирайте число процессов по числу ядер. Избыточное число не даёт дополнительного параллелизма.

7. Измеряйте фактическое ускорение. Накладные расходы могут превышать выигрыш при малом объёме вычислений.

Рекомендации:

  1. Различайте вычислительные задачи и задачи с ожиданием. Первые ускоряются процессами, вторые — потоками.
  2. Учитывайте стоимость сериализации. Объём передаваемых данных влияет на время, поэтому данные делятся на части.
  3. Проверяйте доступность функции в модуле. Функция, определённая внутри другой, не сериализуется по имени.
  4. Не создавайте процесс на каждую задачу. Создание занимает время, поэтому пул переиспользует процессы.
  5. Пересматривайте размер частей. Малые части увеличивают долю накладных расходов, крупные снижают параллелизм.

Практический сценарий: параллельная обработка оказалась медленнее последовательной

Контекст: обработка данных в нескольких процессах выполнялась дольше, чем в одном.

  1. Диагностика. Данные передавались в процесс целиком, а результат возвращался в полном объёме. Время сериализации превышало время вычислений, поэтому общее время выросло.
  2. Причина. Объём передаваемых данных был сопоставим с объёмом вычислений, поэтому накладные расходы на сериализацию и передачу превысили выигрыш от параллелизма.
  3. Действия. (а) Разделили данные на части, чтобы каждая задача обрабатывала собственный фрагмент без передачи всего объёма; (б) сократили объём результата, возвращая агрегированное значение вместо полного набора; (в) использовали пул процессов, чтобы не создавать процесс на каждую задачу; (г) измерили время до и после изменения; (д) определили минимальный объём вычислений, при котором многопроцессность даёт ускорение.
  4. Результат. Время обработки сократилось, поскольку объём передачи мал относительно объёма вычислений.

Вывод: многопроцессность даёт ускорение при объёме вычислений, превышающем накладные расходы на передачу данных. Большой объём сериализации делает параллельную обработку медленнее последовательной.

FAQ

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

Почему функция должна быть определена на уровне модуля? Функция передаётся в процесс по имени, поэтому должна быть доступна в модуле. Функция, определённая внутри другой, не сериализуется по имени.

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

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

Источники: Python multiprocessing, concurrent.futures, pickle — ссылки в шапке.