Пул процессов в Python

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

В предыдущей статье вы узнали, как написать параллельно выполняющийся код, вручную создавая процессы с помощью класса Process из модуля multiprocessing. Но создавать процессы вручную — неэффективно.

Для более эффективного управления процессами можно использовать пул процессов.

Пул процессов — это шаблон для автоматического эффективного управления процессами.

Класс ProcessPoolExecutor из модуля concurrent.futures позволяет создавать и управлять пулом процессов.

Например, класс ProcessPoolExecutor может использовать несколько ядер процессора для создания оптимизированного числа процессов.

ProcessPoolExecutor расширяет класс Executor, у которого есть три метода:

  • submit() — отправляет функцию для выполнения процессом и возвращает объект Future.
  • map() — вызывает функцию для итерируемых объектов.
  • shutdown() — завершает работу исполнителя.

Чтобы освободить ресурсы, удерживаемые исполнителем, необходимо явно вызвать метод shutdown(). Для автоматического отключения исполнителя можно использовать менеджер контекста.

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

  • result() — возвращает результат асинхронной операции.
  • exception() — возвращает исключение, возникшее во время выполнения асинхронной операции.

Пример использования ProcessPoolExecutor

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

Примечание. Убедитесь, что у вас установлена библиотека для обработки изображений Pillow. Установить библиотеку можно так: pip install Pillow.

import time
import os
from concurrent.futures import ProcessPoolExecutor
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()

    with ProcessPoolExecutor() as executor:
        executor.map(create_thumbnail, filenames)
   
    finish = time.perf_counter()

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

Вывод

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

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

1. Объявляем список файлов для создания миниатюр:

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

2. Создаем функцию, которая будет делать из изображения миниатюру и сохранять результат в папке thumbs:

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} обработан...')

3. Создаем пул процессов и вызываем функцию create_thumbnail() для каждого изображения, указанного в списке filenames:

with ProcessPoolExecutor() as executor:
    executor.map(create_thumbnail, filenames)
Проверьте себя
1. Чем ProcessPoolExecutor отличается от ручного создания Process?
AProcessPoolExecutor работает медленнее
BProcessPoolExecutor не поддерживает метод map()
CProcessPoolExecutor автоматически управляет пулом процессов, повторно используя их, что эффективнее ручного создания
DProcessPoolExecutor использует потоки, а не процессы
2. Из какого модуля импортируется ProcessPoolExecutor?
Amultiprocessing
Bthreading
Cconcurrent.futures
Dasyncio
3. Что произойдёт при выходе из блока with ProcessPoolExecutor() as executor?
AПул дождётся завершения всех задач, затем освободит ресурсы
BВсе незавершённые задачи будут немедленно прерваны
CНичего — с пулом нужно работать явно
DПул будет работать в фоне до конца программы
4. Что общего у ProcessPoolExecutor и ThreadPoolExecutor в части интерфейса?
AНичего общего — разные интерфейсы
BОба используют GIL для синхронизации
CОба работают только с I/O-bound задачами
DОба наследуются от Executor и предоставляют методы submit(), map() и shutdown()
py
Курс по теме
Пройдите курс «Python с нуля» — по шагам, с проверкой
8 уроков · ~14 ч · теория, упражнения и экзамен с бейджем
Открыть курс →