Это мой первый пост в мире параллельных вычислений, и в этом посте я хочу рассказать кое-что интересное о параллельном мире в Python. Когда мы попадаем в мир параллельных вычислений, самое популярное словосочетание — потоки. Что такое нити? Потоки выполняют вычисления независимо, и многие задачи могут выполняться потоками параллельно (сокращает время выполнения 🙂). Итак, как вы думаете, использование потоков должно сократить время выполнения! Но, когда мы начинаем параллельное программирование на питоне, то обнаруживаем, что использование потоков достаточно сильно увеличивает время выполнения кода и снижает производительность кода 😦

Я взял пример умножения матриц, чтобы продемонстрировать вышеизложенный факт. Я запустил свою программу на ноутбуке с процессором i5, имеющим версию python 2.7.11+. В этом примере активная часть кода, т. е. часть кода, которая может быть распараллелена, представляет собой вложенные циклы for, в которых каждый поток может вычислять результаты в разных частях массива. Во-первых, я покажу демонстрацию плохой работы модуля потоковой передачи в python. Вот код и время работы.

График времени выполнения кода без использования потоков и с использованием потоков в зависимости от размера ввода (N):

Из приведенного выше графика видно, что использование потоков снижает производительность и увеличивает время работы. Ось Y указывает время в миллисекундах. Синяя линия — обычное матричное умножение, а красная — матричное умножение с использованием потоков. Зазор не маленький. Таким образом, параллельные программисты, использующие python для своих приложений, не могут слепо игнорировать этот факт.

Причиной такой плохой производительности является чудовищный GIL. Потоки в python никогда не используются из-за их плохой производительности, и эта плохая производительность связана с ПЛОХОЙ GLOBAL INTERPRETER LOCK(GIL). GIL запрещает одновременное выполнение всех потоков. Это необходимо для python, так как его управление памятью не является потокобезопасным. Таким образом, использование потоков, т. е. модуль потоковой передачи в python, эквивалентно выполнению кода в последовательном режиме. Потоки никогда не выполняются параллельно. Рисунок ниже помогает визуализировать последовательное выполнение потоков.

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

У каждой проблемы в этом мире есть решение! Каково решение этой проблемы? Модуль многопроцессорности действует как спаситель. Вместо потоков мы должны использовать процессы. В случае процессов нет глобальной блокировки интерпретатора. Таким образом, сокращается время работы и можно использовать параллелизм.

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

График времени выполнения кода без использования процессов и с использованием процессов в зависимости от размера ввода (N):

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

Но есть огромная проблема 😦 Ответ, вычисленный при умножении матриц, неверен. В случае процессов каждый процесс будет иметь свою собственную память, и они не будут совместно использовать память/глобальные переменные. Таким образом, несмотря на то, что вычисления выполняются правильно каждым из процессов, при объединении процессов значения, вычисленные разными процессами, теряются и не отражаются в глобальной переменной. Итак, все, что мы сделали выше, неправильно. Когда мы не получаем правильного ответа, какая польза от прироста производительности?

Но все же, как было сказано выше, для всего существует решение! 😀 Чтобы получить правильный ответ, нам нужно поэкспериментировать.

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

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

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

График времени выполнения кода без использования процессов и с использованием процессов в зависимости от размера ввода (N):

Из приведенного выше графика видно, что использование процессов значительно увеличивает производительность 🙂. Синяя линия — обычное матричное умножение, а красная — матричное умножение с использованием потоков.

Прирост производительности также достигается, и ответ также вычисляется правильно. Нет ничего невозможного 😀 Наконец-то мы смогли добиться того, чего хотели 🙂

В заключение, использование потоков для достижения параллелизма в python — очень плохая идея. Никогда не делай этого. Решение этой проблемы заключается в использовании пула процессов из многопроцессорного модуля и выполнении дополнительной работы, которая объединяет вычисления, выполняемые всеми процессами, для получения правильного ответа. Это использует параллелизм.

Надеюсь, этот пост был достаточно интересным! Мы рассмотрим больше концепций параллелизма в моих последующих постах. Удачного программирования многопроцессорного пула на python 🙂

Первоначально опубликовано на blogonparallelcomputing.wordpress.com 31 января 2017 года.