Модель параллелизма Эрланга более самоуверенна, чем в других языках, где конструкции добавляются в более новых версиях, и многие модели могут сосуществовать. Например, язык C и его библиотеки поддерживают все — от потоков до разветвления новых процессов.
Мнения Эрланга о параллелизме:
- несколько процессов могут работать параллельно все время, даже если они тысячи . Эти процессы могут дать сбой, но независимо.
- Нет общих данных : каждый процесс — это маленький компьютер со своими ресурсами. Нет условий гонки, если нет общих данных.
- Процесс общается через явные сообщения в качестве единственной ссылки. Это немного похоже на отправку пакетов по сети, и на самом деле Эрланг распространен в мире телекоммуникаций.
- Как следствие, связь является асинхронной, поскольку даже то, что известно как удаленный вызов процедуры, должно быть реализовано через пару сообщений, одно для вызова и одно для возвращаемого значения.
Обратите внимание, что на эти тысячи процессов накладных расходов очень мало , поскольку они предоставляются не ОС, а системой исполнения Erlang.
Эрланг следует затем актерской модели. В этой модели все является актером . Это высказывание, подобное тому, что все является объектом , но выполнение в основном параллельное, а не последовательное по умолчанию, как в ОО-приложениях.
Актер может в любое время:
- отправлять сообщения другим актерам, имя которых он знает.
- Получи сообщение и действуй по нему.
- Создавайте новых актеров.
Сравните эту модель с объектами, которые отправляют сообщения друг другу, и создайте новые объекты.
Пример
Параллелизм в ОО-языках, таких как Java, полон синхронизированных ключевых слов для доступа к общим структурам данных, но в модели актора каналы связи между строками кода, принадлежащими различным процессам, являются явными. Даже когда процессы являются локальными, любые данные, передаваемые от одного к другому, будут принудительно преобразованы в конструкцию со специальным синтаксисом.
В этом примере мы хотим вычислить расстояние между двумя точками, каждая из которых представлена в виде списка чисел. Чтобы ускорить процесс (или просто поэкспериментировать с примитивами параллелизма), мы можем распараллелить количество задействованных измерений. Количество измерений составляет всего 2 или 3 в 2D или 3D-графике, но это целесообразно в других приложениях, таких как машинное обучение, где принято представлять права в виде точек в пространстве тысяч измерений.
Спецификация для постановки задачи
Давайте начнем с нашего теста. Мы хотим раскрыть синхронное поведение, чтобы упростить написание тестов:
distance_parallel_test() -> ?assertEqual(0.0, distance_parallel([1], [1])), ?assertEqual(1.0, distance_parallel([2], [3])), ?assertEqual(5.0, distance_parallel([0, 0], [3, 4])), ?assertEqual(2.0, distance_parallel([1, 2, 3, 4], [2, 3, 4, 5])).
Формула для расчета евклидова расстояния — это длина разностного вектора, полученная с помощью теоремы Пифагора.
Мы можем реализовать distance_parallel / 2 в 3 этапа:
- Первые мы икра N новых процессов, один для каждого измерения точки есть.
- затем мы собираем N ответов от этих процессов, каждый из которых представляет квадрат разности между двумя значениями, данными ему.
- Затем мы можем суммировать ответы и извлечь квадратный корень.
Мы могли бы также распараллелить третий этап, но это наша первая попытка параллелизма в Erlang, и это сделало бы пример более неясным.
Это скелет distance_parallel / 2:
distance_parallel(FirstPoint, SecondPoint) -> parallelize_dimensions(FirstPoint, SecondPoint), Squares = collect(length(FirstPoint)), math:sqrt(lists:sum(Squares)).
Распараллеливание
parallelize_dimensions / 2 возьмет первые доступные измерения двух точек, породит новый процесс и вернется с остальными измерениями.
parallelize_dimensions([], []) -> ok; parallelize_dimensions([HeadFirst|TailFirst], [HeadSecond|TailSecond]) -> Pid = self(), spawn(fun() -> difference_squared(HeadFirst, HeadSecond, Pid) end), dimensions(TailFirst, TailSecond).
В spawn () передается анонимная функция, состоящая из вызова diff_squared, которому передаются два соответствующих числа и идентификатор основного процесса.
difference_squared(X1, X2, Destination) -> Difference = X1 - X2, Result = math:pow(Difference, 2), Destination ! {dimension, Result}.
Идентификатор процесса — это кортеж, однозначно идентифицирующий текущий процесс на этом хосте; он может быть получен с помощью self () и затем передан дочернему процессу, как мы сделали бы со ссылкой на объект. Процесс может найти его в глобальной переменной, но с самого начала легче избежать одноэлементного мышления.
Сообщение отправляется другому процессу с помощью! оператор, примененный к идентификатору процесса. Обычно сообщения — это кортежи, первый элемент которых представляет собой атом (теги), поэтому различные типы сообщений можно различить, просто сопоставив первый элемент.
Сборник результатов
Нам также нужно получать сообщения о главном процессе. Хвосто-рекурсивная функция может сделать это, но основное внимание уделяется конструкции получения .
collect(N) -> collect(N, []). collect(0, SquaresAccumulator) -> SquaresAccumulator; collect(N, SquaresAccumulator) -> receive {dimension, Result} -> done end, collect(N - 1, lists:append(SquaresAccumulator, [Result])).
Конструкция работает через сопоставление с образцом и в этом случае принимает только кортежи, содержащие атом измерения ; другие кортежи игнорируются. В этом случае мы просто присваиваем Result часть содержимого сообщения, но вместо возврата done мы можем выполнить код внутри самой конструкции.
Выводы
Вот и все: создание базовых локальных процессов в Erlang — это вопрос объединения spawn / 1, self (), the! оператор и конструкция получения. Когда код заработает, его легко удивить, но он может быть немного разнородным, чтобы сразу его переварить.
Весь код доступен для вашей работы в репозитории этой серии .