Как / почему функциональные языки (в частности, Erlang) хорошо масштабируются?


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

затем, недавно я присутствовал на презентации Кевина Смита "основы Erlang" в Codemash.

Я наслаждался презентацией и узнал, что многие атрибуты функционального программирования позволяют намного легче избежать проблем с потоками/параллелизмом. Я понимаю, что отсутствие состояние и изменчивость делают невозможным для нескольких потоков изменять одни и те же данные, но Кевин сказал (Если я правильно понял), что вся связь происходит через сообщения, и сообщения обрабатываются синхронно (опять же, избегая проблем параллелизма).

но я читал, что Erlang используется в высокомасштабируемых приложениях (вся причина, по которой Ericsson создал его в первую очередь). Как можно эффективно обрабатывать тысячи запросов в секунду, если все обрабатывается как синхронно обработанное сообщение? Разве не поэтому мы начали переходить к асинхронной обработке-чтобы мы могли использовать преимущества одновременного запуска нескольких потоков операций и достижения масштабируемости? Похоже, что эта архитектура, будучи более безопасной, является шагом назад с точки зрения масштабируемости. Чего мне не хватает?

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

как функциональные языки программирования могут быть по своей сути потокобезопасными, но все же масштабироваться?

8 85

8 ответов:

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

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

Это означает, что программист (номинально) не заботится о том, что сообщение будет обработано на другом процессоре или машине: просто отправка сообщения достаточно хороша для его продолжения. Если он заботится об ответе, он будет ждать его как еще одно сообщение.

конечным результатом этого является то, что каждый фрагмент не зависит от любого другого фрагмента. Нет общего кода, нет общего состояния и всех взаимодействий, поступающих из системы сообщений, которые могут быть распределены между многими аппаратными средствами (или нет).

сравните это с традиционной системой: мы должны разместить мьютексы и семафоры вокруг "защищенных" переменных и выполнения кода. У нас есть жесткая привязка в вызове функции через стек (ожидание возврата). Все это создает узкие места, которые меньше проблем в общем ничего похожего на систему Эрланга.

EDIT: я также должен отметить, что Erlang является асинхронным. Вы отправляете свое сообщение, и, возможно,/когда-нибудь другое сообщение возвращается. Или нет.

точка зрения Спенсера о выполнении out of order также важна и хорошо ответила.

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

r = methodWithALotOfDiskProcessing();
x = r + 1;
y = methodWithALotOfNetworkProcessing();
w = x * y

рассмотрим на мгновение, что methodWithALotOfDiskProcessing() занимает около 2 секунд для завершения и что methodWithALotOfNetworkProcessing () занимает около 1 секунды для завершения. В процедурный язык этот код займет около 3 секунд, потому что строки будут выполняться последовательно. Мы теряем время, ожидая завершения одного метода, который может работать одновременно с другим, не конкурируя за один ресурс. В функциональном языке строки кода не диктуют, когда процессор попытается их выполнить. Функциональный язык будет пытаться что-то вроде следующего:

Execute line 1 ... wait.
Execute line 2 ... wait for r value.
Execute line 3 ... wait.
Execute line 4 ... wait for x and y value.
Line 3 returned ... y value set, message line 4.
Line 1 returned ... r value set, message line 2.
Line 2 returned ... x value set, message line 4.
Line 4 returned ... done.

насколько это круто? Идя вперед с кодом и только ждать при необходимости мы сократили время ожидания до двух секунд автоматически! :D Так что да, хотя код является синхронным, он, как правило, имеет другое значение, чем в процедурных языках.

EDIT:

Как только вы поймете эту концепцию в сочетании с сообщением Годеке, легко представить, как простой это становится, чтобы воспользоваться преимуществами нескольких процессоров, ферм серверов, избыточных хранилищ данных и кто знает, что еще.

вполне вероятно, что вы путаете синхронно С последовательный.

тело функции в Erlang обрабатывается последовательно. Так что то, что сказал Спенсер об этом "автоматическом эффекте", не соответствует действительности для Эрланга. Однако вы можете смоделировать это поведение с эрлангом.

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

-module(countwords).
-export([count_words_in_lines/1]).

count_words_in_lines(Lines) ->
    % For each line in lines run spawn_summarizer with the process id (pid)
    % and a line to work on as arguments.
    % This is a list comprehension and spawn_summarizer will return the pid
    % of the process that was created. So the variable Pids will hold a list
    % of process ids.
    Pids = [spawn_summarizer(self(), Line) || Line <- Lines], 
    % For each pid receive the answer. This will happen in the same order in
    % which the processes were created, because we saved [pid1, pid2, ...] in
    % the variable Pids and now we consume this list.
    Results = [receive_result(Pid) || Pid <- Pids],
    % Sum up the results.
    WordCount = lists:sum(Results),
    io:format("We've got ~p words, Sir!~n", [WordCount]).

spawn_summarizer(S, Line) ->
    % Create a anonymous function and save it in the variable F.
    F = fun() ->
        % Split line into words.
        ListOfWords = string:tokens(Line, " "),
        Length = length(ListOfWords),
        io:format("process ~p calculated ~p words~n", [self(), Length]),
        % Send a tuple containing our pid and Length to S.
        S ! {self(), Length}
    end,
    % There is no return in erlang, instead the last value in a function is
    % returned implicitly.
    % Spawn the anonymous function and return the pid of the new process.
    spawn(F).

% The Variable Pid gets bound in the function head.
% In erlang, you can only assign to a variable once.
receive_result(Pid) ->
    receive
        % Pattern-matching: the block behind "->" will execute only if we receive
        % a tuple that matches the one below. The variable Pid is already bound,
        % so we are waiting here for the answer of a specific process.
        % N is unbound so we accept any value.
        {Pid, N} ->
            io:format("Received \"~p\" from process ~p~n", [N, Pid]),
            N
    end.

и вот как это выглядит, когда мы запускаем это в оболочке:

Eshell V5.6.5  (abort with ^G)
1> Lines = ["This is a string of text", "and this is another", "and yet another", "it's getting boring now"].
["This is a string of text","and this is another",
 "and yet another","it's getting boring now"]
2> c(countwords).
{ok,countwords}
3> countwords:count_words_in_lines(Lines).
process <0.39.0> calculated 6 words
process <0.40.0> calculated 4 words
process <0.41.0> calculated 3 words
process <0.42.0> calculated 4 words
Received "6" from process <0.39.0>
Received "4" from process <0.40.0>
Received "3" from process <0.41.0>
Received "4" from process <0.42.0>
We've got 17 words, Sir!
ok
4> 

ключевая вещь, которая позволяет масштабировать Erlang, связана с параллелизмом.

операционная система обеспечивает параллелизм с помощью двух механизмов:

  • процессы операционной системы
  • потоки операционной системы

процессы не разделяют состояние – один процесс не может разбить другой по дизайну.

потоки разделяют состояние – один поток может разбить другой по дизайну-это ваша проблема.

С Erlang – один рабочий системный процесс используется виртуальной машиной, и виртуальная машина обеспечивает параллелизм программе Erlang не с помощью потоков операционной системы, а с помощью процессов Erlang – то есть Erlang реализует свой собственный timeslicer.

эти процессы Erlang разговаривают друг с другом, отправляя сообщения (обрабатываются виртуальной машиной Erlang, а не операционной системой). Процессы Erlang обращаются друг к другу с помощью идентификатора процесса (PID), который имеет трехчастный адрес <<N3.N2.N1>>:

  • процесс № N1 на
  • VM N2 on
  • физическая машина N3

два процесса на одной виртуальной машине, на разных виртуальных машинах на одной машине или на двух машинах взаимодействуют одинаково – поэтому масштабирование не зависит от количества физических машин, на которых вы развертываете свое приложение (в первом приближении).

Erlang является только потокобезопасным в тривиальном смысле – у него нет потоков. (Язык, то есть SMP / многоядерная виртуальная машина использует одну операционную систему потока на ядро).

У вас может быть непонимание того, как Эрланг работает. Среда выполнения Erlang минимизирует переключение контекста на ЦП, но если доступно несколько ЦП, то все они используются для обработки сообщений. У вас нет "потоков" в том смысле, что вы делаете в других языках, но вы можете иметь много сообщений, обрабатываемых одновременно.

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

похоже, вы перепутали синхронный и последовательный, как Крис упомянул.

в чисто функциональном языке порядок оценки не имеет значения - в приложении функции fn (arg1, .. argn), N аргументов могут быть вычислены параллельно. Это гарантирует высокий уровень (автоматического) параллелизма.

Erlang использует модель процесса, где процесс может выполняться на той же виртуальной машине или на другом процессоре-нет никакого способа сказать. Это возможно только потому, что сообщения копируются между процессами, нет общего (изменяемого) состояния. Многопроцессорный паралеллизм идет намного дальше, чем многопоточность, поскольку потоки зависят от общей памяти, это может быть только 8 потоков, работающих параллельно на 8-ядерном процессоре, в то время как многопроцессорная обработка может масштабироваться до тысяч параллельных процессов.