Взаимодействие с параллельным миром

Эта выдержка охватывает:

  • Повышение производительности за счет одновременного выполнения
  • Масштабирование наших программ
  • Почему стоит выбрать Go для параллелизма
  • Программирование параллельных решений для создания более гибкого, высокопроизводительного и масштабируемого программного обеспечения.

Читайте дальше, чтобы узнать больше.

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

Увеличение пропускной способности

В нашей повседневной жизни мы постоянно имеем дело с параллелизмом. Каждый раз, когда мы садимся за руль своего автомобиля, мы взаимодействуем с несколькими действующими лицами, такими как другие автомобили, велосипедисты и пешеходы. Мы можем легко прогнозировать и ориентироваться в такой среде. На работе мы откладываем задачу, пока ждем ответа по электронной почте, и беремся за следующую. Готовя, мы планируем свои шаги, чтобы максимизировать нашу производительность и сократить время. Нашему мозгу совершенно комфортно управлять одновременным поведением. На самом деле, он делает это все время, а мы даже не замечаем этого.
Параллельное программирование — это написание инструкций, чтобы несколько задач и процессов могли выполняться и взаимодействовать одновременно. Что произойдет, если два покупателя сделают заказ одновременно, а в наличии останется только один товар? Если цена авиабилета растет каждый раз, когда клиент покупает билет, что происходит, когда несколько билетов бронируются одновременно? Если у нас внезапно возрастет нагрузка из-за дополнительного спроса, как будет масштабироваться наше программное обеспечение, когда мы увеличим ресурсы обработки и памяти? Все это примеры сценариев, с которыми сталкиваются разработчики при разработке и программировании параллельного программного обеспечения.

Для современного разработчика все важнее понимать, как программировать параллельно. Это связано с тем, что с годами аппаратный ландшафт изменился в пользу этого типа программирования.
До появления многоядерной технологии производительность процессоров увеличивалась пропорционально тактовой частоте и количеству транзисторов, примерно удваиваясь каждые 2 года. Разработчики процессоров начали достигать физических пределов из-за перегрева и энергопотребления, что совпало со взрывным ростом мобильного оборудования, такого как ноутбуки и смартфоны. Чтобы снизить чрезмерный расход заряда аккумулятора и перегрев ЦП при одновременном повышении вычислительной мощности, инженеры внедрили многоядерные процессоры.
Кроме того, с ростом популярности облачных вычислений разработчики получили легкий доступ к большим и дешевым вычислительным ресурсам для выполнения своего кода. Вся эта дополнительная вычислительная мощность может быть эффективно использована только в том случае, если наш код написан таким образом, чтобы в полной мере использовать преимущества дополнительных процессорных блоков.

Повышение скорости отклика

Наличие нескольких ресурсов обработки означает, что мы можем масштабироваться по горизонтали. Мы можем использовать дополнительные процессоры для параллельных вычислений и быстрее завершать наши задачи. Это возможно только в том случае, если мы напишем код таким образом, чтобы в полной мере использовать дополнительные вычислительные ресурсы.
А как насчет системы с одним процессором? Есть ли какое-либо преимущество в написании параллельного кода, когда наша система не имеет нескольких процессоров? Оказывается, даже в этом сценарии написание параллельных программ выгодно.
Большинство программ тратят лишь небольшую часть своего времени на выполнение вычислений на процессоре. Подумайте, например, о текстовом процессоре, который ожидает ввода с клавиатуры. Или утилита поиска текстовых файлов, тратящая большую часть своего времени на ожидание загрузки частей текстовых файлов в память. Мы можем заставить нашу программу выполнять другую задачу, пока она ожидает ввода/вывода. Например, текстовый процессор может выполнять проверку орфографии в документе, пока пользователь думает о том, что печатать дальше. Мы можем заставить утилиту поиска файлов искать совпадение с файлом, который мы уже загрузили в память, пока мы ждем окончания чтения следующего файла в другую часть памяти.
Подумайте, например, когда мы готовим или испечь наше любимое блюдо. Мы можем более эффективно использовать свое время, если, пока блюдо стоит в духовке или плите, вместо того, чтобы бездельничать и просто ждать, будем выполнять какие-то другие действия. Таким образом, мы более эффективно используем наше время и работаем более продуктивно. Это аналогично нашей системе, выполняющей другие инструкции на ЦП, в то время как та же самая программа одновременно ожидает завершения сетевого сообщения, пользовательского ввода или записи файла. Это означает, что наша программа может выполнять больше работы за то же время.

Параллельное программирование делает наше программное обеспечение более отзывчивым, потому что нам не нужно ждать завершения одной задачи, прежде чем реагировать на ввод пользователя. Даже если у нас есть один процессор, мы всегда можем приостановить выполнение набора инструкций, ответить на ввод пользователя, а затем продолжить выполнение, ожидая ввода следующего пользователя.
Если снова подумать текстового процессора, в фоновом режиме могут выполняться несколько задач, пока мы печатаем. Есть задача, которая прослушивает события клавиатуры и отображает каждый символ на экране. У нас может быть другая задача, которая проверяет нашу орфографию и грамматику в фоновом режиме. Возможно, запущена другая задача, чтобы предоставить нам статистику по нашему документу (количество слов, страниц и т. д.). Все эти задачи, выполняемые вместе, создают впечатление, что они каким-то образом выполняются одновременно. Происходит то, что эти различные задачи быстро переключаются операционной системой на ЦП. На рис. 4 показана упрощенная временная шкала, показывающая выполнение этих трех задач на одном процессоре. Эта система чередования реализована с использованием комбинации аппаратных прерываний и ловушек операционной системы. Мы более подробно рассмотрим операционные системы и параллелизм в следующей главе. Сейчас важно понимать, что если бы у нас не было этой системы чередования, нам пришлось бы выполнять каждую задачу одну за другой. Нам нужно было бы ввести предложение, затем нажать кнопку проверки орфографии, дождаться ее завершения, а затем нажать другую кнопку и дождаться появления статистики документа.

Параллельное программирование в Go

Go — идеальный язык для изучения параллельного программирования, потому что его создатели разработали его с учетом высокопроизводительного параллелизма. Цель состояла в том, чтобы создать язык, который был бы эффективным во время выполнения, удобочитаемым и простым в использовании. Это означает, что в Go есть много инструментов для параллельного программирования. Давайте рассмотрим некоторые преимущества использования Go для параллельных программ.

Краткий обзор горутин

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

Моделирование параллелизма с CSP и примитивами

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

Создание собственных инструментов параллелизма

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

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

Масштабирование производительности

Знание того, как использовать эти инструменты параллелизма, хорошо, но как насчет знания внутренней работы? Мы собираемся сделать еще один шаг и использовать подход создания их вместе с нуля. Знание внутренней работы параллельного программирования необходимо для получения максимальной отдачи от него.
Мы выберем общие инструменты параллелизма и поймем, как их можно реализовать, используя другие примитивы параллелизма в качестве строительных блоков. Например, в Go нет встроенной реализации семафоров, поэтому помимо понимания того, как и когда использовать семафоры, мы реализуем их сами. Мы делаем это также для некоторых инструментов, доступных в Go, таких как группы ожидания и каналы. Идея состоит в том, что если мы знаем, как что-то построить, мы также будем знать, как и когда это использовать.

  • Распознавание и предотвращение распространенных проблем параллельного программирования, таких как взаимоблокировки и условия гонки.
  • Управление параллелизмом с использованием горутин, мьютексов, блокировок чтения-записи, семафоров, групп ожидания, барьеров, каналов, условных переменных и способов создания некоторых из этих инструментов.
  • Выявление шаблонов параллелизма, таких как конвейеризация, рабочие пулы, общая память и передача сообщений.
  • Обнаружение преимуществ, ограничений и свойств параллельных вычислений.
  • Улучшение навыков кодирования на Go с более сложными многопоточными темами.
  • И многое другое!
  • Параллельное программирование позволяет нам создавать более отзывчивое программное обеспечение.

Масштабируемость производительности — это мера того, насколько хорошо наша программа ускоряется пропорционально увеличению количества ресурсов, доступных для программы. Чтобы понять это, давайте попробуем использовать простую аналогию.
Представьте себе мир, в котором мы являемся застройщиками. Наш текущий активный проект – строительство небольшого многоэтажного жилого дома. Отдаем ваш архитектурный план строительнице, и она отправляется достраивать домик. Все работы завершены в течение 8 месяцев.
Как только мы закончим, мы получим еще один запрос на точно такую ​​же сборку, но в другом месте. Чтобы ускорить процесс, мы нанимаем двух строителей вместо одного. Во второй раз строители завершат дом всего за 4 месяца.
В следующий раз, когда мы получим еще один проект по строительству точно такого же дома, мы соглашаемся нанять еще больше помощников, чтобы дом был закончен быстрее. На этот раз мы платим 4 строителям, и на их выполнение уходит 2 с половиной месяца. Дом стоил нам немного больше, чтобы построить, чем предыдущий. Оплата 4 строителям в течение 2,5 месяцев обходится вам дороже, чем оплата 2 строителям в течение 4 месяцев (при условии, что все они взимают одинаковую плату).
Опять же, мы повторяем эксперимент еще дважды, один раз с 8 строителями и еще раз с 16. С обоими 8 и 16 строителей, дом строили 2 месяца. Кажется, что сколько бы рук мы ни вложили в работу, сборка не может быть завершена быстрее, чем за 2 месяца. Говоря гиковским языком, мы говорим, что достигли своего предела масштабируемости. Почему это вообще происходит? Почему мы не можем продолжать удваивать наши ресурсы (люди/деньги/процессоры) и всегда сокращать затрачиваемое время наполовину?

Закон Амдала

В 1967 году Джин Амдал, ученый-компьютерщик, представил на конференции формулу, которая измеряла ускорение в зависимости от соотношения параллельных и последовательных задач. Это известно как закон Амдала.

Закон Густафсона

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

В нашем сценарии построения дома масштабируемость здесь ограничена различными факторами. Во-первых, наш подход к решению проблемы может ограничивать нас. Например, нельзя построить второй этаж, не закончив первый. Кроме того, несколько частей сборки можно делать только последовательно. Например, если к строительной площадке ведет только одна дорога, то только один транспорт может использовать эту дорогу в любой момент времени. Другими словами, некоторые части процесса сборки выполняются последовательно (одна за другой), а другие части могут выполняться параллельно (одновременно). Эти факторы влияют на масштабируемость нашей задачи и ограничивают ее.
Закон Амдала говорит нам, что непараллельные части выполнения действуют как узкое место и ограничивают преимущества распараллеливания выполнения. На рис. 6 показано соотношение между теоретическим ускорением, полученным при увеличении числа процессоров.

Если мы применим эту диаграмму к нашей проблеме строительства, когда мы используем одного строителя и он тратит 5% своего времени на части, которые могут быть выполнены только последовательно, масштабируемость соответствует самой верхней строке нашей диаграммы (параллельность 95%). Эта последовательная часть может быть выполнена только одним человеком, например, перевозка строительных материалов грузовиком по узкой дороге.
Как видно из диаграммы, даже с 512 людьми, работающими на строительстве, мы только закончить работу примерно в 19 раз быстрее, чем если бы у нас был всего один человек. После этого ситуация не сильно улучшается. Нам понадобится более 4096 строителей, чтобы закончить проект всего в 20 раз быстрее. Мы достигли жесткого предела вокруг этого числа. Наем большего числа рабочих совсем не помогает, и мы будем тратить деньги впустую.
Ситуация еще хуже, если параллелизуется меньший процент работы. С 90% мы достигнем этого предела масштабируемости намного быстрее, около отметки 512 рабочих. С 75% мы получаем 128, а с 50% всего 16. Обратите также внимание, что снижается не только этот предел, но и значительно снижается ускорение. В 90%, 75% и 50% мы получаем максимальное ускорение в 10, 4 и 2 раза соответственно.
Закон Амдала рисует довольно мрачную картину параллельного программирования и параллельных вычислений. Даже с параллельным кодом, который имеет небольшую долю последовательной обработки, масштабируемость значительно снижается. К счастью, это не полная картина.

В 1988 году двое ученых-компьютерщиков, Джон Л. Густафсон и Эдвин Х. Барсис, пересмотрели закон Амдала и опубликовали статью, посвященную некоторым из его недостатков. Это дает альтернативный взгляд на пределы параллелизма. Их главный аргумент заключается в том, что на практике размер проблемы меняется, когда у нас есть доступ к большему количеству ресурсов.
Продолжая аналогию со строительством дома, если бы в нашем распоряжении были тысячи строителей, это было бы расточительно вкладывать их всех в строительство небольшого дома, когда у нас есть другие будущие проекты в разработке. Вместо этого мы бы попытались направить оптимальное количество строителей на строительство нашего дома, а остальных рабочих распределить на другие проекты.
Если бы мы разрабатывали какое-то программное обеспечение и у нас было бы большое количество вычислительных ресурсов , если бы мы заметили, что использование половины ресурсов привело к той же производительности этого программного обеспечения, мы могли бы выделить эти дополнительные ресурсы для выполнения других задач, таких как повышение точности или качества этого программного обеспечения в других областях.
Второй момент. против закона Амдала состоит в том, что при увеличении размера задачи непараллельная часть задачи обычно не растет пропорционально размеру задачи. Фактически, Густафсон утверждает, что для многих задач это остается постоянным. Таким образом, если принять во внимание эти два момента, ускорение может масштабироваться линейно в зависимости от доступных параллельных ресурсов. Это соотношение показано на рисунке 7.

Сводка

Закон Густафсона говорит нам, что пока мы находим способы занять наши дополнительные ресурсы, ускорение должно продолжать расти и не ограничиваться последовательной частью проблемы. Это возможно только в том случае, если последовательная часть остается постоянной по мере увеличения размера задачи, что, согласно Густафсону, имеет место во многих типах программ.
Чтобы полностью понять законы Амдала и Густафсона, давайте возьмем компьютерную игру в качестве пример. Допустим, конкретная компьютерная игра с богатой графикой была написана таким образом, чтобы использовать несколько вычислительных процессоров. Со временем компьютеры становятся все более параллельными вычислительными ядрами, и мы можем запускать ту же игру с более высокой частотой кадров, что обеспечивает более плавный игровой процесс. В конце концов мы доходим до точки, когда мы добавляем больше процессоров, но частота кадров больше не увеличивается. Это происходит, когда мы достигаем предела скорости. Независимо от того, сколько процессоров мы добавим, игра не будет работать с более высокой частотой кадров. Именно об этом говорит нам закон Амдала — что существует предел ускорения для конкретной задачи фиксированного размера, если она имеет непараллельную часть.
Однако по мере совершенствования технологий и увеличения числа ядер процессоров разработчики игр используйте эти дополнительные процессоры с пользой. Хотя частота кадров может не увеличиться, игра теперь может содержать больше графических деталей и более высокое разрешение благодаря дополнительной вычислительной мощности. Это закон Густафсона в действии. Когда мы увеличиваем ресурсы, ожидается увеличение возможностей системы, и разработчики будут эффективно использовать дополнительную вычислительную мощность.

Подробнее о книге «здесь».

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

  • Параллельные программы также могут обеспечить повышенное ускорение при работе на нескольких процессорах.
  • Мы также можем увеличить производительность, даже если у нас есть один процессор, если наше параллельное программирование эффективно использует время ожидания ввода/вывода.
  • Go предоставляет нам горутины, которые представляют собой легкие конструкции для моделирования параллельных исполнений.
  • Go предоставляет нам абстракции, такие как каналы, которые позволяют параллельным выполнениям взаимодействовать и синхронизироваться.
  • Go позволяет нам выбрать создание нашего параллельного приложения либо с использованием модели параллельных последовательных процессов (CSP), либо с использованием классических примитивов.
  • Используя модель CSP, мы уменьшаем вероятность некоторых типов одновременных ошибок; однако некоторые задачи могут выполняться более эффективно, если мы используем классические примитивы.
  • Закон Амдала говорит нам, что масштабируемость производительности задачи фиксированного размера ограничена непараллельными частями выполнения.
  • Закон Густафсона говорит нам, что если мы продолжаем находить способы занять наши дополнительные ресурсы, ускорение должно продолжать увеличиваться и не ограничиваться последовательной частью задачи.
  • Познакомьтесь с Джейн Саттон. Джейн работает в международной бухгалтерской компании HSS разработчиком программного обеспечения всего 3 месяца. В своем последнем проекте она столкнулась с проблемой, возникшей в системе начисления заработной платы. Это программный модуль, который запускается в конце месяца, после закрытия рабочего дня, и вычисляет все выплаты заработной платы сотрудников клиентов HSS. Сегодня утром ее менеджер организовал встречу с владельцем продукта, командой инфраструктуры и торговым представителем, чтобы попытаться разобраться в сути проблемы. Неожиданно Сарика Кумар, технический директор, присоединилась к переговорной по видеосвязи.
    Томас Бок, владелец продукта, начинает: «Я не понимаю. Модуль расчета заработной платы работал нормально, сколько я себя помню. Внезапно в прошлом месяце расчеты по платежам не были завершены вовремя, и мы получили массу жалоб от наших клиентов. Это заставило нас выглядеть действительно непрофессионально с Block Entertainment, нашим новым и самым крупным клиентом, угрожающим перейти к нашему конкуренту».
    Менеджер Джейн, Франческо Варезе, вмешивается: занять слишком много времени. Они медленные из-за своей сложной природы, учитывая множество факторов, таких как отсутствие сотрудников, даты вступления, сверхурочные и тысячи других факторов. Часть программного обеспечения была написана более десяти лет назад на C++. В фирме не осталось ни одного разработчика, понимающего, как работает этот код».
    «Мы собираемся подписать контракт с нашим крупнейшим клиентом — компанией, в которой работает более 30 000 человек. Они слышали о нашей проблеме с заработной платой и хотят, чтобы она была решена, прежде чем заключать контракт. Очень важно, чтобы мы исправили это как можно скорее», — отвечает Роб Горналл из отдела продаж и закупок.
    «Мы попытались добавить больше процессорных ядер и памяти на сервер, на котором работает модуль. Это не имело абсолютно никакого значения. Когда мы выполняем расчет заработной платы с использованием тестовых данных, это занимает одинаковое количество времени, независимо от того, сколько ресурсов мы выделяем. Расчет заработной платы всех клиентов занимает более 20 часов, что слишком поздно для наших клиентов», — продолжает Фрида Норберг из отдела инфраструктуры.
    Наконец, настала очередь Джейн говорить. Как новый сотрудник фирмы, она немного колеблется, но умудряется сказать: «Если код написан таким образом, чтобы не использовать преимущества дополнительных ядер, не имеет значения, если вы выделите для него несколько процессоров. Код должен использовать параллельное программирование, чтобы он работал быстрее, когда вы добавляете к нему дополнительные ресурсы обработки. ”
    Кажется, все установили, что Джейн лучше всех разбирается в предмете. Наступает короткая пауза. Джейн чувствует, что все хотят, чтобы она придумала какой-то ответ, поэтому она продолжает: «Правильно. Хорошо… Я экспериментировал с простой программой, написанной на Go. Он делит платежные ведомости на более мелкие группы сотрудников, а затем вызывает модуль расчета заработной платы с каждой группой в качестве входных данных. Я запрограммировал его так, чтобы он одновременно вызывал модуль, используя несколько горутин. Я также использую канал Go для балансировки рабочей нагрузки. В конце концов у меня есть еще одна горутина, которая собирает результаты по другому каналу».
    Джейн быстро оглядывается и видит только пустое выражение на лицах всех, поэтому она добавляет: «В симуляциях это как минимум в 5 раз быстрее на одном и том же многоядерное оборудование. Есть еще несколько тестов, которые нужно выполнить, чтобы убедиться, что нет условий гонки, но я почти уверен, что смогу заставить его работать еще быстрее, особенно если я получу помощь от бухгалтерии, чтобы перенести старую логику C++ в чистый параллельный код Go. .”
    На лице менеджера Джейн появилась широкая улыбка. Все остальные на встрече кажутся удивленными и безмолвными. В конце концов технический директор говорит: «Джейн, что тебе нужно, чтобы сделать это к концу месяца?»
    Практические знания в области параллельного программирования становятся все более востребованными навыками в технологических компаниях. Предприятия хотят использовать свои аппаратные ресурсы на полную мощность, поскольку это экономит их время и деньги. Для этого они понимают, что должны нанимать подходящих специалистов — разработчиков, которые могут писать масштабируемые параллельные приложения.
    Помимо найма или продвижения в качестве разработчиков, знание параллельного программирования дает нам более широкий набор навыков, которые мы можем использовать в новых приложениях. сценарии. Например, мы можем моделировать сложные бизнес-взаимодействия, которые происходят одновременно. Мы также можем использовать параллельное программирование для повышения скорости отклика нашего программного обеспечения за счет быстрого выполнения задач. В отличие от последовательного программирования, параллельное программирование может использовать несколько ядер ЦП. Это позволяет нам увеличить работу, выполняемую нашей программой, за счет ускорения выполнения программы. Попробуем рассмотреть некоторые из этих сценариев более подробно.

Современный параллелизм с Go