Як / чому функціональні мови (зокрема Erlang) добре масштабуються?


92

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

Тоді нещодавно я відвідав презентацію Кевіна Сміта "Основи Ерланга" у Codemash .

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

Але я вже читав, що Erlang використовується у дуже масштабованих додатках (вся причина, по якій Ericsson створив його в першу чергу). Як може бути ефективно обробляти тисячі запитів в секунду, якщо все обробляється як синхронно оброблене повідомлення? Чи не тому ми почали рухатися до асинхронної обробки - щоб ми могли скористатися перевагами запуску декількох потоків операцій одночасно і досягти масштабованості? Здається, що ця архітектура, хоча і безпечніша, є кроком назад з точки зору масштабованості. Чого мені не вистачає?

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

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


1
[Не згадується]: ВМ Ерлангса піднімає асинхронність на інший рівень. За допомогою магії вуду (asm) він дозволяє виконувати такі операції синхронізації, як socket: читання для блокування без зупинки потоку os. Це дозволяє писати синхронний код, коли інші мови примушуватимуть вас до гнізд асинхронного зворотного виклику. Набагато простіше написати програму масштабування з уявною картиною однопотокових мікрослуг VS, маючи на увазі загальну картину кожного разу, коли ви щось прикріплюєте до основи коду.
Vans S

@Vans S Цікаво.
Джим Андерсон,

Відповіді:


99

Функціональна мова (загалом) не покладається на мутацію змінної. Через це нам не потрібно захищати "спільний стан" змінної, оскільки значення є фіксованим. Це, у свою чергу, дозволяє уникнути більшості стрибків, які традиційні мови повинні пройти, щоб реалізувати алгоритм на процесорах або машинах.

Ерланг робить це далі, ніж традиційні функціональні мови, випікаючи систему передачі повідомлень, яка дозволяє усьому працювати в системі, заснованій на подіях, де фрагмент коду турбується лише про отримання повідомлень та надсилання повідомлень, не турбуючись про загальну картину.

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

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

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

EDIT: Я також повинен зазначити, що Erlang є асинхронним. Ви надсилаєте своє повідомлення, і можливо / колись надійде інше повідомлення. Чи ні.

Побачення Спенсера щодо невиконання замовлення також є важливим і має на нього відповідь.


Я це розумію, але не бачу, наскільки ефективна модель повідомлення. Я б здогадався про протилежне. Для мене це справді відкриває очі. Недарма функціональні мови програмування приділяють стільки уваги.
Джим Андерсон,

3
Ви отримуєте великий потенціал одночасності у спільній системі нічого. Погана реалізація (наприклад, надсилання високих повідомлень) може торпедувати це, але, здається, Ерланг все зробив правильно і зберігав усе на малій вазі.
Годеке

Важливо зазначити, що, хоча Erlang має семантику передачі повідомлень, вона має реалізацію спільної пам'яті, отже, вона має описану семантику, але вона не копіює матеріали всюди, якщо це не потрібно.
Аарон Маенпаа

1
@Godeke: "Erlang (як і більшість функціональних мов) зберігає один примірник будь-яких даних, коли це можливо". AFAIK, Ерланг насправді глибоко копіює все, що пройшло між його легкими процесами через відсутність одночасної ГХ.
JD

1
@JonHarrop майже правий: коли процес надсилає повідомлення іншому процесу, повідомлення копіюється; за винятком великих двійкових файлів, які передаються за посиланням. Дивіться, наприклад, jlouisramblings.blogspot.hu/2013/10/embrace-copying.html, чому це добре.
hcs42

74

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

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

Подумайте на мить, що методWithALotOfDiskProcessing () займає близько 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 Отже, так, хоча код є синхронним, він, як правило, має інше значення, ніж у процедурних мовах.

РЕДАГУВАТИ:

Ознайомившись із цією концепцією разом із дописом Годеке, можна легко уявити, наскільки просто стає користуватися перевагами декількох процесорів, ферм серверів, надлишкових сховищ даних і хто знає, що ще.


Класно! Я абсолютно неправильно зрозумів, як обробляються повідомлення. Дякую, ваш допис допомагає.
Джим Андерсон,

"Функціональна мова спробує щось на зразок наступного" - Я не впевнений в інших функціональних мовах, але в Erlang приклад буде працювати точно так само, як у випадку процедурних мов. Ви можете виконувати ці два завдання паралельно, нерестуючи процеси, дозволяючи їм виконувати ці два завдання асинхронно та отримуючи їх результати в кінці, але це не так, як "в той час як код синхронний, він, як правило, має інше значення, ніж у процедурних мовах. " Дивіться також відповідь Кріса.
hcs42

16

Ймовірно, ви змішуєте синхронне з послідовним .

Тіло функції в 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> 

13

Ключова річ, яка дозволяє масштабувати Erlang, пов’язана з паралельністю.

Операційна система забезпечує паралельність за допомогою двох механізмів:

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

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

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

З Erlang - віртуальна машина використовує один процес операційної системи, і віртуальна машина забезпечує паралельність програми Erlang не за допомогою потоків операційної системи, а шляхом надання процесів Erlang - тобто Erlang реалізує власний таймслікер.

Ці процеси Erlang спілкуються між собою, надсилаючи повідомлення (обробляється Erlang VM, а не операційною системою). Процеси Erlang звертаються один до одного за допомогою ідентифікатора процесу (PID), який має тридільну адресу <<N3.N2.N1>>:

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

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

Ерланг є лише безпечним у тривіальному сенсі - він не має ниток. (Мова, тобто SMP / багатоядерна віртуальна машина використовує один потік операційної системи на ядро).


7

Можливо, у вас є нерозуміння того, як працює Ерланг. Час виконання Erlang мінімізує перемикання контексту на центральному процесорі, але якщо доступно декілька процесорів, то всі вони використовуються для обробки повідомлень. У вас немає "потоків" у тому сенсі, як у вас іншими мовами, але ви можете одночасно обробляти багато повідомлень.


4

Повідомлення Erlang є чисто асинхронними, якщо ви хочете синхронну відповідь на ваше повідомлення, вам потрібно чітко кодувати це. Можливо, було сказано, що повідомлення у вікні повідомлень про процес обробляються послідовно. Будь-яке повідомлення, надіслане процесу, переходить до цього вікна повідомлення процесу, і процес вибирає одне повідомлення з цього вікна, обробляє його, а потім переходить до наступного, у порядку, який вважає за потрібне. Це дуже послідовний акт, і блок отримання робить саме це.

Схоже, ви змішали синхронне та послідовне, як згадав Кріс.



-2

Суто функціональною мовою порядок оцінки значення не має - у додатку функції fn (arg1, .. argn) n аргументів можна оцінювати паралельно. Це гарантує високий рівень (автоматичного) паралелізму.

Erlang використовує модель процесу, де процес може запускатися на одній і тій же віртуальній машині або на іншому процесорі - про це неможливо сказати. Це можливо лише тому, що повідомлення копіюються між процесами, і немає спільного (змінного) стану. Багатопроцесорний паралелізм йде набагато далі, ніж багатопотоковість, оскільки потоки залежать від спільної пам'яті, це може бути лише 8 потоків, що працюють паралельно на 8-ядерному процесорі, тоді як багатопроцесорність може масштабуватися до тисяч паралельних процесів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.