Многопроцессорность в Python

В этой статье вы узнаете, как выполнять код параллельно с помощью модуля multiprocessing в Python.

Обычно программы имеют дело с двумя типами задач:

  • Связанные с вводом/выводом (I/O bound). Если задача выполняет много операций ввода/вывода, она называется задачей, связанной с вводом/выводом.
    Примеры: чтение из файлов, запись в файлы, подключение к базам данных и выполнение сетевого запроса.
    Ускорить выполнение можно с помощью многопоточности.
  • Привязанные к процессору (CPU bound). Когда задача выполняет много операций с использованием процессора, она называется задачей, привязанной к процессору.
    Примеры: изменение размера изображения и потоковое видео — это задачи, привязанные к процессору.
    Ускорить выполнение можно с помощью многопроцессорности.

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

Пример мультипроцессорной программы

import time

def task(n=100_000_000):
    while n:
        n -= 1

if __name__ == '__main__':
    start = time.perf_counter()
    task()
    task()
    finish = time.perf_counter()

    print(f'Выполнение заняло {finish-start: .2f} секунд.')

Вывод

Выполнение заняло 12.94 секунд.

Как это работает

1. Определим функцию task(), которая имеет большой цикл while от 100 миллионов до 0. Функция task() привязана к процессору, потому что она выполняет вычисления.

def task(n=100_000_000):
    while n:
        n -= 1

2.  Дважды вызовем функцию task() и засечем время обработки:

if __name__ == '__main__':
    start = time.perf_counter()
    task()
    task()
    finish = time.perf_counter()

    print(f'Выполнение заняло {finish-start: .2f} секунд.')

Используем модуль multiprocessing

В следующей программе используется модуль multiprocessing — и она выполняется быстрее:

import time
import multiprocessing

def task(n=100_000_000):
    while n:
        n -= 1

if __name__  ==  '__main__':
    start = time.perf_counter()

    p1 = multiprocessing.Process(target=task)
    p2 = multiprocessing.Process(target=task)

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    finish = time.perf_counter()

    print(f'Выполнение заняло {finish-start: .2f} секунд.')

Вывод

Выполнение заняло 6.45 секунд.

Как это работает

1. Импортируем модуль multiprocessing.

import multiprocessing

2. Создадим два процесса и передадим функцию task() каждому из них:

p1 = multiprocessing.Process(target=task)
p2 = multiprocessing.Process(target=task)

Примечание. Конструктор Process() возвращает новый объект Process.

3. Вызовем метод start() объектов Process для запуска процесса:

p1.start()
p2.start()

4. Дожидаемся завершения процессов с помощью метода join():

p1.join()
p2.join()

Практический пример программы с многопроцессорностью

Мы будем использовать модуль multiprocessing для изменения размера изображений высокого разрешения.

Для начала установим библиотеку Pillow для обработки изображений:

pip install Pillow

Теперь напишем программу, которая создает миниатюры изображений в папке images и сохраняет их в папке thumbs:

import time
import os
from PIL import Image, ImageFilter

filenames = [
    'images/1.jpg',
    'images/2.jpg',
    'images/3.jpg',
    'images/4.jpg',
    'images/5.jpg',
]

def create_thumbnail(filename, size=(50,50), thumb_dir ='thumbs'):
    img = Image.open(filename)
    img = img.filter(ImageFilter.GaussianBlur())
    img.thumbnail(size)
    img.save(f'{thumb_dir}/{os.path.basename(filename)}')
    print(f'Файл {filename} обработан...')


if __name__ == '__main__':
    start = time.perf_counter()

    for filename in filenames:
        create_thumbnail(filename)
        
    finish = time.perf_counter()

    print(f'Выполнение заняло {finish-start: .2f} секунд.')

Вывод

Файл images/1.jpg обработан...
Файл images/2.jpg обработан...
Файл images/3.jpg обработан...
Файл images/4.jpg обработан...
Файл images/5.jpg обработан...
Выполнение заняло 1.28 секунд.

Теперь модифицируем программу для использования многопроцессорной обработки. Каждый процесс будет создавать миниатюру изображения:

import time
import os
import multiprocessing
from PIL import Image, ImageFilter


filenames = [
    'images/1.jpg',
    'images/2.jpg',
    'images/3.jpg',
    'images/4.jpg',
    'images/5.jpg',
]

def create_thumbnail(filename, size=(50,50),thumb_dir ='thumbs'):
    img = Image.open(filename)
    img = img.filter(ImageFilter.GaussianBlur())
    img.thumbnail(size)
    img.save(f'{thumb_dir}/{os.path.basename(filename)}')
    print(f'Файл {filename} обработан...')


if __name__ == '__main__':
    start = time.perf_counter()

    # создаем процесс
    processes = [multiprocessing.Process(target=create_thumbnail, args=[filename]) 
                for filename in filenames]

    # запускаем процесс
    for process in processes:
        process.start()

    # дожидаемся выполнение
    for process in processes:
        process.join()

    finish = time.perf_counter()

    print(f'Выполнение заняло {finish-start: .2f} секунд.')

Вывод

Файл images/5.jpg обработан...
Файл images/4.jpg обработан...
Файл images/1.jpg обработан...
Файл images/3.jpg обработан...
Файл images/2.jpg обработан...
Выполнение заняло 0.82 секунд.

При использовании многопроцессорности программа обрабатывает изображения гораздо быстрее.

Что нужно запомнить

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

Проверьте себя
1. Почему многопроцессорность (multiprocessing) подходит для CPU-bound задач лучше, чем многопоточность?
AПроцессы работают на разных ядрах процессора в реальном параллелизме, не ограниченном GIL
BПроцессы занимают меньше памяти, чем потоки
CПроцессы автоматически распределяют нагрузку между ядрами без кода
DПотоки не могут выполнять математические вычисления
2. Какой API у multiprocessing.Process аналогичен threading.Thread?
AОдинаковый: Process(target=fn), p.start(), p.join()
BСовершенно разный — методы называются иначе
CProcess.run() вместо Thread.start()
DProcess использует with-синтаксис, Thread — нет
3. Зачем в программе с multiprocessing нужен блок if __name__ == '__main__'?
AЭто просто стиль кодирования без практического смысла
BЧтобы ускорить запуск программы
CЧтобы защитить от рекурсивного создания процессов при импорте модуля дочерними процессами
DЧтобы указать Python, где начинается основная программа
4. Какое примерное ускорение даёт выполнение двух CPU-bound задач в двух процессах на двухъядерном процессоре?
AНикакого — multiprocessing не даёт прироста на Python
BПримерно в 2 раза
CПримерно в 4 раза
DПримерно в 10 раз
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →