Статьи

Erlang: параллелизм

Модель параллелизма Эрланга более самоуверенна, чем в других языках, где конструкции добавляются в более новых версиях, и многие модели могут сосуществовать. Например, язык 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! оператор и конструкция получения. Когда код заработает, его легко удивить, но он может быть немного разнородным, чтобы сразу его переварить.

Весь код доступен для вашей работы в репозитории этой серии .