Руководство по многопроцессорной обработке и параллельному программированию в Python

Персонализированный диспетчер задач на Python Изучение

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

Введение в параллелизм

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

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

Давайте пока примем это за общий принцип, но позже в этой статье мы увидим, что это не всегда верно.

Не вдаваясь в подробности, идея параллелизма состоит в том, чтобы написать код таким образом, чтобы он мог использовать несколько ядер ЦП.

Чтобы упростить задачу, давайте рассмотрим пример.

Параллельные и последовательные вычисления

Представьте, что вам нужно решить огромную проблему, и вы одиноки. Вам нужно вычислить квадратный корень из восьми различных чисел. Что вы делаете? Ну, у тебя не так много вариантов. Вы начинаете с первого числа и вычисляете результат. Затем вы идете дальше с остальными.

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

Читайте также:  Установить Hyper Terminal на Ubuntu?

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

В этих примерах каждый друг представляет ядро ​​процессора

Модели для параллельного программирования

Мы установили, что такое параллельное программирование, но как мы его используем? Что ж, мы уже говорили ранее, что параллельные вычисления включают выполнение нескольких задач между несколькими ядрами процессора, что означает, что эти задачи выполняются одновременно. Есть несколько вопросов, которые вы должны рассмотреть, прежде чем приступать к распараллеливанию. Например, есть ли другие оптимизации, которые могли бы ускорить наши вычисления?

А пока примем как должное, что распараллеливание — лучшее решение для вас. Существуют в основном три модели параллельных вычислений:

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

В этой статье мы проиллюстрируем первую модель, которая также является самой простой.

Многопроцессорность Python: параллелизм на основе процессов в Python

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

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

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

Преимущества использования многопроцессорности

Вот несколько преимуществ многопроцессорной обработки:

  • лучшее использование ЦП при работе с задачами с высокой нагрузкой на ЦП
  • больший контроль над дочерним элементом по сравнению с потоками
  • легко кодировать

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

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

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

Начало работы с многопроцессорной обработкой Python

Наконец-то мы готовы написать код на Python!

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

  • Процесс parent. Существует только один родительский процесс, у которого может быть несколько дочерних процессов.
  • Процесс child. Это порождается родителем. У каждого ребенка также могут быть новые дети.

Мы собираемся использовать childпроцесс для выполнения определенной функции. Таким образом, parentможно продолжить его выполнение.

Простой пример многопроцессорной обработки Python

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

from multiprocessing import Process

def bubble_sort(array):
    check = True
    while check == True:
      check = False
      for i in range(0, len(array)-1):
        if array[i] > array[i+1]:
          check = True
          temp = array[i]
          array[i] = array[i+1]
          array[i+1] = temp
    print("Array sorted: ", array)

if __name__ == '__main__':
    p = Process(target=bubble_sort, args=([1,9,4,5,2,6,8,4],))
    p.start()
    p.join()

В этом фрагменте мы определили функцию с именем bubble_sort(array). Эта функция является действительно наивной реализацией алгоритма сортировки пузырьком. Если вы не знаете, что это такое, не волнуйтесь, потому что это не так важно. Важно знать, что это функция, которая выполняет некоторую работу.

Класс процесса

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

  • target=bubble_sort, что означает, что наш новый процесс будет запускать bubble_sortфункцию
  • args=([1,9,4,52,6,8,4],), который представляет собой массив, переданный в качестве аргумента целевой функции

После того, как мы создали экземпляр класса Process, нам нужно только запустить процесс. Это делается письменно p.start(). В этот момент процесс запущен.

Перед выходом нам нужно дождаться, пока дочерний процесс завершит свои вычисления. Метод join()ожидает завершения процесса.

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

Класс пула

Что, если нам нужно создать несколько процессов для выполнения задач, требующих больше ресурсов ЦП? Всегда ли нам нужно начинать и явно ждать завершения? Решение здесь состоит в том, чтобы использовать Poolкласс.

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

from multiprocessing import Pool
import time
import math

N = 5000000

def cube(x):
    return math.sqrt(x)

if __name__ == "__main__":
    with Pool() as pool:
      result = pool.map(cube, range(10,N))
    print("Program finished!")

В этом фрагменте кода у нас есть cube(x)функция, которая просто принимает целое число и возвращает его квадратный корень. Легко, верно?

Затем мы создаем экземпляр Poolкласса без указания какого-либо атрибута. Класс пула по умолчанию создает один процесс на ядро ​​ЦП. Далее мы запускаем mapметод с несколькими аргументами.

Метод mapприменяет cubeфункцию к каждому элементу предоставленного нами итерируемого объекта, который в данном случае представляет собой список всех чисел от 10до N.

Огромным преимуществом этого является то, что вычисления в списке выполняются параллельно!

Лучшее использование многопроцессорной обработки Python

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

Чтобы убедить вас в этом, давайте рассмотрим простой пример:

from multiprocessing import Pool
import time
import math

N = 5000000

def cube(x):
    return math.sqrt(x)

if __name__ == "__main__":
    # first way, using multiprocessing
    start_time = time.perf_counter()
    with Pool() as pool:
      result = pool.map(cube, range(10,N))
    finish_time = time.perf_counter()
    print("Program finished in {} seconds - using multiprocessing".format(finish_time-start_time))
    print("---")
    # second way, serial computation
    start_time = time.perf_counter()
    result = []
    for x in range(10,N):
      result.append(cube(x))
    finish_time = time.perf_counter()
    print("Program finished in {} seconds".format(finish_time-start_time))

Этот фрагмент основан на предыдущем примере. Мы решаем ту же задачу, что и вычисление квадратного корня из Nчисел, но двумя способами. Первый предполагает использование многопроцессорности Python, а второй — нет. Мы используем perf_counter()метод из timeбиблиотеки для измерения производительности по времени.

На моем ноутбуке я получаю такой результат:

> python code.py
Program finished in 1.6385094 seconds - using multiprocessing
---
Program finished in 2.7373942999999996 seconds

Как видите, разница больше секунды. Так что в этом случае многопроцессорность лучше.

Давайте изменим что-нибудь в коде, например, значение N. Давайте понизим его до N=10000и посмотрим, что произойдет.

Вот что я получаю сейчас:

> python code.py
Program finished in 0.3756742 seconds - using multiprocessing
---
Program finished in 0.005098400000000003 seconds

Что случилось? Кажется, что многопроцессорность сейчас плохой выбор. Почему?

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

Заключение

В этой статье мы говорили об оптимизации производительности кода Python с помощью многопроцессорной обработки Python.

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

Оцените статью
bestprogrammer.ru
Добавить комментарий