Краткий обзор параллельного программирования

Параллелизм — одна из наиболее распространенных и важных концепций программирования, которая в наши дни становится все более популярной. Это еще один ресурс, естественно поддерживаемый некоторыми языками программирования (например, NodeJS, Go), что упрощает его использование. Параллелизм позволяет определить набор задач, которые должны выполняться независимо (по крайней мере, до определенного момента), чтобы ни одна из них не блокировала другие.

Реализуя параллельное поведение, программа может значительно улучшить производительность, время выполнения и использование ресурсов.

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

Пример №1: Расчет общей суммы

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

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

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

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

Пример №2: выполнение четырех независимых задач

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

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

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

Выводы

Из этих довольно простых экспериментов мы могли бы сделать некоторые выводы об использовании параллелизма:

Из примера №1

Для небольших коллекций однопоточный (без параллелизма) подход кажется более быстрым, чем другие. Это связано с тем, что каждый рабочий процесс должен быть настроен языком программирования, что часто включает в себя вызов процесса ОС, выделение памяти и синхронизацию каждого из них.

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

Однако мы видим, что 3 и 5 кажутся гораздо более разумными значениями для numWorkers, так как с ними работа по настройке занимает меньше времени, чем с 10.

Из примера №2

Больше нечего сказать. Учитывая, что все задачи независимы, лучше всего выполнять их одновременно. И полученный график говорит сам за себя: параллельный подход занимает почти на 50% меньше времени, чем однопоточный.

Старайтесь использовать параллелизм разумно. Он привлекает внимание программистов как очень современный и модный ресурс, но он может не подходить для всех случаев, с которыми вы сталкиваетесь. Обратите внимание на те, которые состоят из множества задач, которые можно выполнять независимо и которые предполагают выполнение огромного объема работы. Для зависимых задач вы столкнетесь с ограничениями в использовании параллелизма, а для быстрых задач (например, суммирования 1000 чисел в коллекции) его использовать не имеет смысла.

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