Как/почему функциональные языки (в частности, Erlang) хорошо масштабируются?
Я наблюдал за растущей видимостью функциональных языков программирования и функций некоторое время. Я посмотрел на них и не видел причины для обращения.
Затем я недавно присутствовал на презентации Кевина Смита "Основы Эрланг" на Codemash.
Мне понравилась презентация и я узнал, что многие атрибуты функционального программирования значительно облегчают проблемы с потоками / concurrency. Я понимаю, что отсутствие состояния и изменчивость не позволяют нескольким потокам изменять одни и те же данные, но Кевин сказал (если я правильно понял), всякое общение происходит через сообщения, а сообщения обрабатываются синхронно (опять же избегая проблем concurrency).
Но я читал, что Erlang используется в высокомасштабируемых приложениях (вся причина, по которой Ericsson создала его в первую очередь). Как можно эффективно обрабатывать тысячи запросов в секунду, если все обрабатывается как синхронно обработанное сообщение? Не поэтому мы начали двигаться в направлении асинхронной обработки - чтобы мы могли одновременно использовать несколько потоков работы и добиться масштабируемости? Кажется, что эта архитектура, будучи более безопасной, является шагом назад в плане масштабируемости. Что мне не хватает?
Я понимаю, что создатели Erlang намеренно избегали поддержки потоков, чтобы избежать проблем с w391, но я думал, что для достижения масштабируемости необходима многопоточность.
Как функциональные языки программирования по своей сути являются потокобезопасными, но все же масштабируются?
Ответы
Ответ 1
Функциональный язык не (в общем) не полагается на мутацию переменной. Из-за этого нам не нужно защищать "общее состояние" переменной, потому что значение фиксировано. Это, в свою очередь, позволяет избежать большинства прыжков с обручем, которые должны пройти традиционные языки, чтобы реализовать алгоритм между процессорами или машинами.
Erlang берет его дальше, чем традиционные функциональные языки, путем выпечки в системе передачи сообщений, которая позволяет всем функционировать в системе на основе событий, где часть кода беспокоится только о получении сообщений и отправке сообщений, не беспокоясь о более крупном изображении.
Это означает, что программист (номинально) не заботится о том, чтобы сообщение обрабатывалось на другом процессоре или машине: просто отправка сообщения достаточно для продолжения. Если он заботится о ответе, он будет ждать его в качестве другого сообщения.
Конечным результатом этого является то, что каждый фрагмент не зависит от любого другого фрагмента. Нет общего кода, никакого общего состояния и всех взаимодействий, поступающих из системы сообщений, которые могут быть распределены между многими аппаратными средствами (или нет).
Сравните это с традиционной системой: мы должны помещать мьютексы и семафоры вокруг "защищенных" переменных и выполнения кода. У нас есть жесткая привязка в вызове функции через стек (ожидание возврата). Все это создает узкие места, которые являются менее сложными в общей системе ничего, вроде Erlang.
EDIT: Я должен также указать, что Erlang является асинхронным. Вы отправляете свое сообщение, и, возможно, когда-нибудь придет еще одно сообщение. Или нет.
Спенсеровская точка о выполнении вне порядка также важна и хорошо ответила.
Ответ 2
Система очереди сообщений классная, потому что она эффективно создает эффект "огонь-и-ожидание-результат", который является синхронной частью, о которой вы читаете. Что делает это невероятно удивительным, так это то, что строки не обязательно должны выполняться последовательно. Рассмотрим следующий код:
r = methodWithALotOfDiskProcessing();
x = r + 1;
y = methodWithALotOfNetworkProcessing();
w = x * y
Рассмотрим на какой-то момент, что methodWithALotOfDiskProcessing() занимает около 2 секунд, и этот методWithALotOfNetworkProcessing() занимает около 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:
Как только вы поймете эту концепцию в сочетании с почтой Godeke, легко представить, насколько простым становится использование нескольких процессоров, ферм серверов, избыточных хранилищ данных и кто знает, что еще.
Ответ 3
Вероятно, вы смешиваете синхронный с последовательным.
Тело функции в 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 getting boring now"].
["This is a string of text","and this is another",
"and yet another","it 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>
Ответ 4
Ключевое значение, которое позволяет масштабировать Erlang, связано с concurrency.
Операционная система предоставляет concurrency двумя механизмами:
- процессы операционной системы.
- потоки операционной системы
Процессы не разделяют состояние - один процесс не может сбой другого по дизайну.
Состояние потока потоков - один поток может привести к сбою другого по дизайну - вот ваша проблема.
С помощью Erlang - виртуальная машина использует один процесс операционной системы, а VM предоставляет concurrency программе Erlang не с использованием потоков операционной системы, а путем предоставления процессов Erlang, то есть Erlang реализует свой собственный временной журнал.
Эти процессы Erlang разговаривают друг с другом, отправляя сообщения (обрабатываемые виртуальной машиной Erlang, а не операционной системой). Процессы Erlang обращаются друг к другу с использованием идентификатора процесса (PID), который имеет трехкомпонентный адрес <<N3.N2.N1>>
:
- процесс нет N1 на
- VM N2 on
- физическая машина N3
Два процесса на одной виртуальной машине, на разных виртуальных машинах на одном компьютере или на двух машинах обмениваются одинаково - поэтому масштабирование не зависит от количества физических машин, на которых вы развертываете приложение (в первом приближении).
Erlang только потокобезопасен в тривиальном смысле - в нем нет нитей. (Язык, на котором SMP/многоядерная VM использует один поток операционной системы для ядра).
Ответ 5
У вас может быть непонимание того, как работает Erlang. Время выполнения Erlang сводит к минимуму переключение контекста на CPU, но если доступно несколько процессоров, все они используются для обработки сообщений. У вас нет "потоков" в том смысле, что вы делаете это на других языках, но вы можете обрабатывать много сообщений одновременно.
Ответ 6
Ссылочная прозрачность: см. http://en.wikipedia.org/wiki/Referential_transparency_(computer_science)
Ответ 7
Сообщения Erlang являются чисто асинхронными, если вы хотите получить синхронный ответ на свое сообщение, вам необходимо явно указать его. Возможно, было сказано, что сообщения в окне сообщений процесса обрабатываются последовательно. Любое сообщение, отправленное процессу, находится в этом окне сообщения процесса, и процесс получает, чтобы выбрать одно сообщение из этого окна, и затем перейти к следующему, в том порядке, в котором он сочтет нужным. Это очень последовательный акт, и блок приема выполняет именно это.
Похоже, вы смешались синхронно и последовательно, как упоминал Крис.
Ответ 8
В чисто функциональном языке порядок оценки не имеет значения - в приложении-функции fn (arg1,.. argn) n аргументов можно оценивать параллельно. Это гарантирует высокий уровень (автоматический) parallelism.
Erlang использует модем процесса, в котором процесс может выполняться на той же виртуальной машине или на другом процессоре - нет способа рассказать. Это возможно только потому, что сообщения копируются между процессами, нет общего (изменяемого) состояния. Многопроцессорный паралеллизм идет намного дальше, чем многопоточность, поскольку потоки зависят от разделяемой памяти, это может быть только 8 потоков, работающих параллельно на 8-ядерном процессоре, в то время как многопроцессорная обработка может масштабироваться до тысяч параллельных процессов.