関数型言語(具体的にはErlang)はどのように/なぜ適切にスケーリングしますか?


92

しばらくの間、関数型プログラミング言語と機能の可視性の高まりを見てきました。私はそれらを調べましたが、上訴の理由はわかりませんでした。

その後、最近、CodemashでのKevin Smithの "Basics of Erlang"プレゼンテーションに参加しました

私はプレゼンテーションを楽しみ、関数型プログラミングの属性の多くにより、スレッド化/同時実行の問題を回避することがはるかに容易になることを学びました。状態と可変性の欠如により、複数のスレッドが同じデータを変更することが不可能になることを理解していますが、Kevinは(正しく理解していれば)すべての通信はメッセージを通じて行われ、メッセージは同期して処理されます(ここでも同時実行の問題を回避します)。

しかし、Erlangが非常にスケーラブルなアプリケーションで使用されていることを読んだことがあります(Ericssonが最初にそれを作成した理由の全体)。すべてが同期的に処理されるメッセージとして処理される場合、1秒あたり数千のリクエストを効率的に処理する方法を教えてください。それが、非同期処理に移行し始めた理由ではありませんか?複数の操作スレッドを同時に実行することを利用して、スケーラビリティを実現できますか?このアーキテクチャはより安全ですが、スケーラビリティの点で後退しているようです。何が欠けていますか?

Erlangの作成者が並行性の問題を回避するために意図的にスレッド化のサポートを回避したことは理解していますが、スケーラビリティを実現するにはマルチスレッド化が必要だと思いました。

関数型プログラミング言語はどのようにして本質的にスレッドセーフでありながら、まだ拡張できるのでしょうか。


1
[言及なし]:ErlangsのVMは非同期性を別のレベルに引き上げます。ブードゥーマジック(asm)により、osスレッドを停止せずに、socket:readのような同期操作をブロックできます。これにより、他の言語が非同期コールバックのネストを強制するときに同期コードを書くことができます。コードベースに何かを追加するたびに、全体像を念頭に置いて、シングルスレッドのマイクロサービスVSの心構えを持つスケーリングアプリを書く方がはるかに簡単です。
Vans S

@Vans S興味深い。
ジムアンダーソン

回答:


97

関数型言語は、(一般的に)に依存しない変異変数を。このため、値が固定されているため、変数の「共有状態」を保護する必要はありません。これにより、プロセッサまたはマシン間でアルゴリズムを実装するために従来の言語が通過しなければならない大部分のフープジャンプが回避されます。

Erlangは、従来の関数型言語よりもさらに進んで、メッセージベースのシステムですべてを操作できるようにするメッセージパッシングシステムを組み込んでいます。コードの一部は、メッセージの受信と送信のみを心配し、全体像を心配しません。

これは、プログラマーが(名目上)メッセージが別のプロセッサーまたはマシンで処理されることを(名目上)心配していないことを意味します。単にメッセージを送信するだけで、メッセージを継続できます。応答を気にする場合は、別のメッセージとして待機します

この結果、各スニペットは他のすべてのスニペットから独立しています。共有コード、共有状態、メッセージシステムからのすべてのやり取りは、多くのハードウェアに分散できます(または分散できません)。

これを従来のシステムと比較してください。「保護された」変数とコード実行の周りにミューテックスとセマフォを配置する必要があります。スタックを介した関数呼び出しに緊密なバインディングがあります(戻りが発生するのを待っています)。これらすべてが、Erlangのようなシェアードナッシングシステムでは問題の少ないボトルネックを作り出します。

編集:私はまた、Erlangが非同期であることを指摘する必要があります。あなたはあなたのメッセージを送り、多分/いつか別のメッセージが戻ってきます。か否か。

順不同の実行についてのスペンサーのポイントも重要であり、よく答えられています。


私はこれを理解していますが、メッセージモデルがどのように効率的かはわかりません。私は反対だと思います。これは私にとって本当に目を見張るものです。関数型プログラミング言語がそれほど注目されているのも不思議ではありません。
ジムアンダーソン

3
シェアードナッシングシステムでは、多くの同時実行の可能性が得られます。悪い実装(たとえば、メッセージパッシングのオーバーヘッドが高い)はこれを魚雷で飛ばす可能性がありますが、Erlangはそれを正しく行い、すべてを軽量に保ちます。
Godeke 2009年

Erlangにはメッセージパッシングセマンティクスがある一方で、共有メモリの実装があるため、説明されているセマンティクスを持っていますが、必要がない場合は両方ですべてをコピーするわけではないことに注意することが重要です。
アーロンメンパー09年

1
@Godeke:「Erlang(ほとんどの関数型言語と同様)は、可能な場合、データの単一インスタンスを保持します。」AFAIK、Erlangは実際には同時GCがないため、軽量プロセス間で渡されるすべてをディープコピーします。
JD

1
@JonHarropはほぼ正しいです。プロセスが別のプロセスにメッセージを送信すると、メッセージがコピーされます。参照によって渡される大きなバイナリを除きます。これが良いことである理由については、たとえばjlouisramblings.blogspot.hu/2013/10/embrace-copying.htmlを参照してください。
hcs42 2015年

73

メッセージキューシステムは優れています。これは、「結果を取得して待機する」という効果を生み出し、これが現在読んでいる同期部分です。これが信じられないほど素晴らしいのは、行を順番に実行する必要がないことです。次のコードを検討してください。

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

methodWithALotOfDiskProcessing()が完了するまでに約2秒かかり、methodWithALotOfNetworkProcessing()が完了するまでに約1秒かかることを少し考えてみてください。手続き型言語では、行が順次実行されるため、このコードの実行には約3秒かかります。単一のリソースを求めて競合することなく、他のメソッドと同時に実行できる1つのメソッドが完了するのを待つ時間を無駄にしています。関数型言語では、コード行は、プロセッサがいつそれらを試行するかを指示しません。関数型言語は次のようなことを試みます。

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.

なんてクールなの?コードを先に進め、必要な場合にのみ待機することで、待機時間を自動的に2秒に短縮しました。:Dつまり、コードは同期ですが、手続き型言語とは異なる意味を持つ傾向があります。

編集:

Godekeの投稿と併せてこの概念を理解すれば、マルチプロセッサ、サーバーファーム、冗長データストアを活用することがいかに簡単になり、他に何を知っているかを想像するのは簡単です。


涼しい!メッセージがどのように処理されているのか、私は完全に誤解していました。ありがとう、あなたの投稿は役に立ちます。
ジムアンダーソン

「関数型言語は次のようなものを試します」-他の関数型言語についてはわかりませんが、Erlangの例は手続き型言語の場合とまったく同じように機能します。プロセスを生成し、2つのタスクを非同期で実行させ、最後に結果を取得することで、これら2つのタスクを並行して実行できますが、「コードが同期している間は、手続き型言語とは異なる意味を持つ傾向があります。 」クリスの回答もご覧ください。
hcs42 2015年

16

同期シーケンシャルを混同している可能性があります。

erlangの関数の本体は順次処理されています。したがって、この「自動効果」についてスペンサーが言ったことは、アーランには当てはまりません。この動作をerlangでモデル化することもできます。

たとえば、1行の単語数を計算するプロセスを生成できます。複数の行があるため、行ごとにそのようなプロセスを1つ生成し、その行から合計を計算するための回答を受け取ります。

このようにして、「重い」計算を行うプロセス(可能な場合は追加のコアを利用)を生成し、後で結果を収集します。

-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のスケーリングを可能にする重要なことは、並行性に関連しています。

オペレーティングシステムは、2つのメカニズムによって同時実行性を提供します。

  • オペレーティングシステムプロセス
  • オペレーティングシステムスレッド

プロセスは状態を共有しません。あるプロセスが別のプロセスをクラッシュさせることはありません。

スレッドは状態を共有します-あるスレッドが設計上別のスレッドをクラッシュさせる可能性があります-それがあなたの問題です。

Erlangでは、1つのオペレーティングシステムプロセスが仮想マシンによって使用され、VMはオペレーティングシステムスレッドを使用するのではなく、Erlangプロセスを提供することによってErlangプログラムに並行性を提供します。つまり、Erlangは独自のタイムスライサーを実装します。

これらのErlangプロセスは、メッセージを送信することによって互いに対話します(オペレーティングシステムではなくErlang VMによって処理されます)。Erlangプロセスは、3つの部分からなるアドレスを持つプロセスID(PID)を使用して互いにアドレスを指定します<<N3.N2.N1>>

  • N1を処理しない
  • VM N2がオン
  • 物理マシンN3

同じVM上の2つのプロセス、同じマシン上の2つのVM、または2つのマシンは同じ方法で通信します。したがって、スケーリングは、アプリケーションをデプロイする物理マシンの数に依存しません(最初の概算)。

Erlangは些細な意味でのみスレッドセーフです–スレッドはありません。(つまり、SMP /マルチコアVMはコアごとに1つのオペレーティングシステムスレッドを使用します)。


7

Erlangの動作について誤解しているかもしれません。ErlangランタイムはCPUでのコンテキスト切り替えを最小限に抑えますが、複数のCPUが利用可能な場合は、すべてがメッセージの処理に使用されます。他の言語で行うような「スレッド」はありませんが、多数のメッセージを同時に処理することができます。


4

Erlangメッセージは完全に非同期です。メッセージへの同期応答が必要な場合は、明示的にコーディングする必要があります。おそらく言われたことは、プロセスメッセージボックス内のメッセージが順番に処理されるということでした。プロセスに送信されたメッセージはそのプロセスメッセージボックスに置かれ、プロセスはそのボックスから1つのメッセージを選択して処理し、適切と思われる順序で次のメッセージに移動します。これは非常に順次的な行為であり、受信ブロックはまさに​​それを行います。

chrisが言ったように、同期と順次を混同しているようです。



-2

純粋に関数型の言語では、評価の順序は重要ではありません。関数アプリケーションfn(arg1、.. argn)では、n個の引数を並行して評価できます。これにより、高レベルの(自動)並列処理が保証されます。

Erlangはプロセスモデルを使用しており、プロセスは同じ仮想マシンで実行することも、別のプロセッサで実行することもできます。これは、メッセージがプロセス間でコピーされるためにのみ可能であり、共有(可変)状態はありません。スレッドは共有メモリに依存しているため、マルチプロセッサの並列処理はマルチスレッドよりもはるかに遠くにあります。これは、8コアのCPUで並列に実行できるスレッドは8つだけですが、マルチ処理は数千の並列プロセスにスケーリングできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.