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

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

Затем я недавно посетил презентацию Кевина Смита "Основы Эрланга" на Codemash .

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

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

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

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

Ответы [ 8 ]

92 голосов
/ 24 января 2009

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

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

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

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

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

РЕДАКТИРОВАТЬ: Я должен также указать, что Эрланг является асинхронным. Вы отправляете свое сообщение и, возможно, когда-нибудь придет другое сообщение. Или нет.

Замечание Спенсера о неисполнении заказа также важно и хорошо ответило.

71 голосов
/ 24 января 2009

Система очереди сообщений - это круто, потому что она эффективно производит эффект «запуска и ожидания результата», который является синхронной частью, о которой вы читаете. То, что делает это невероятно удивительным, это то, что это означает, что строки не должны выполняться последовательно. Рассмотрим следующий код:

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:

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

15 голосов
/ 05 февраля 2009

Вероятно, вы смешиваете синхронно с последовательно .

Тело функции в erlang обрабатывается последовательно. Так что то, что Спенсер сказал об этом "автоматическом эффекте", не относится к эрлангу. Вы можете смоделировать это поведение с помощью 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> 
12 голосов
/ 24 января 2009

Ключевой момент, который позволяет Эрлангу масштабироваться, связан с параллелизмом.

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

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

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

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

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

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

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

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

Erlang является лишь поточно-ориентированным в тривиальном смысле - у него нет потоков. (Язык SMP / многоядерный VM использует один поток операционной системы на ядро).

6 голосов
/ 24 января 2009

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

3 голосов
/ 10 февраля 2009

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

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

2 голосов
/ 24 января 2009

Ссылочная прозрачность: см. http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)

0 голосов
/ 24 января 2009

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

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

...