しばらくの間、関数型プログラミング言語と機能の可視性の高まりを見てきました。私はそれらを調査しましたが、上訴の理由はわかりませんでした。
それから、最近、私は Codemash でのKevin Smithの "Basics of Erlang"プレゼンテーションに出席しました。
私はプレゼンテーションを楽しみ、関数型プログラミングの属性の多くにより、スレッド化/同時実行の問題を回避することがはるかに容易になることを学びました。状態と可変性の欠如により、複数のスレッドが同じデータを変更することが不可能になることを理解していますが、ケビン氏は(正しく理解していれば)すべての通信はメッセージを通じて行われ、メッセージは同期して処理されます(ここでも同時実行の問題を回避します)。
しかし、Erlangが非常にスケーラブルなアプリケーションで使用されていることを私は読んだ(Ericssonがそもそもそれを作成した理由全体)。すべてが同期的に処理されるメッセージとして処理される場合、1秒あたり数千の要求を効率的に処理するにはどうすればよいでしょうか。それが非同期処理に移行し始めた理由ではありませんか?それで、複数の操作スレッドを同時に実行することを利用してスケーラビリティを実現できますか?このアーキテクチャはより安全ですが、スケーラビリティの点で後退しているようです。何が欠けていますか?
Erlangの作成者が並行性の問題を回避するために意図的にスレッド化のサポートを回避したことは理解していますが、スケーラビリティを実現するにはマルチスレッド化が必要だと思いました。
関数型プログラミング言語はどのようにして本質的にスレッドセーフでありながら、それでもスケーリングできますか?
関数型言語は、(一般に)変数の変更に依存しません。このため、値が固定されているため、変数の「共有状態」を保護する必要はありません。これにより、プロセッサまたはマシン間でアルゴリズムを実装するために従来の言語が通過しなければならない大部分のジャンプを回避できます。
Erlangは、従来の関数型言語よりもさらに進んで、メッセージ受け渡しシステムですべてを操作できるようにすることで、コードの一部がメッセージの受信と送信のみを心配し、全体像を気にすることなく、すべてをイベントベースのシステムで操作できるようにします。
これは、プログラマーが(名目上)メッセージが別のプロセッサーまたはマシンで処理されることを(名目上)気にしていないことを意味します。単にメッセージを送信するだけで十分です。応答を気にする場合は、別のメッセージとして待機します。
この結果、各スニペットは他のすべてのスニペットから独立しています。共有コード、共有状態、メッセージシステムからのすべてのやり取りは、多くのハードウェアに分散できます(または分散できません)。
これを従来のシステムと比較してください。「保護された」変数とコード実行の周りにミューテックスとセマフォを配置する必要があります。スタックを介した関数呼び出しに緊密なバインディングがあります(戻りが発生するのを待っています)。これらすべてが、Erlangのようなシェアードナッシングシステムでは問題の少ないボトルネックを作り出します。
編集:私はまた、Erlangが非同期であることを指摘する必要があります。あなたはあなたのメッセージを送り、多分/いつか別のメッセージが戻ってきます。か否か。
順不同の実行に関するスペンサーのポイントも重要であり、よく答えられています。
メッセージキューシステムはクールです。これは、読んでいる同期部分である「結果を待つ」結果を効果的に生成するためです。これが信じられないほど素晴らしいのは、行を順番に実行する必要がないことです。次のコードを検討してください。
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の投稿と併せてこの概念を理解すると、simpleがどのようにマルチプロセッサ、サーバーファーム、冗長データストアを利用し、他に何を知っているかを想像するのは簡単です。
synchronousとsequentialを混同している可能性があります。
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>
Erlangのスケーリングを可能にする重要な点は、並行性に関連しています。
オペレーティングシステムは、次の2つのメカニズムによって同時実行性を提供します。
プロセスは状態を共有しません。あるプロセスが別のプロセスをクラッシュさせることはありません。
スレッドは状態を共有します–あるスレッドが別のスレッドをクラッシュさせる可能性があります–それがあなたの問題です。
Erlangでは、1つのオペレーティングシステムプロセスが仮想マシンによって使用され、VMはオペレーティングシステムスレッドを使用するのではなく、Erlangプロセスを提供することによってErlangプログラムに同時実行性を提供します。つまり、Erlangは独自のタイムスライサーを実装します。
これらのErlangプロセスは、メッセージを送信することで互いに対話します(Erlang VMオペレーティングシステムでは処理されません)。Erlangプロセスは、3つの部分からなるプロセスID(PID)を使用して互いにアドレスを指定します。住所 <<N3.N2.N1>>
:
同じVM上の2つのプロセス、同じマシン上の2つのVM、または2つのマシンは同じ方法で通信します。したがって、スケーリングは、アプリケーションをデプロイする物理マシンの数に依存しません(最初の概算)。
Erlangは些細な意味でのみスレッドセーフです–スレッドはありません。 (つまり、SMP /マルチコアVMはコアごとに1つのオペレーティングシステムスレッドを使用します)。
Erlangの動作について誤解しているかもしれません。 ErlangランタイムはCPUでのコンテキスト切り替えを最小限に抑えますが、複数のCPUが利用可能な場合は、すべてがメッセージの処理に使用されます。他の言語で行うような「スレッド」はありませんが、多数のメッセージを同時に処理することができます。
Erlangメッセージは完全に非同期です。メッセージへの同期応答が必要な場合は、明示的にコーディングする必要があります。おそらく言われたことは、プロセスメッセージボックス内のメッセージが順番に処理されるということでした。プロセスに送信されたメッセージはそのプロセスメッセージボックスに置かれ、プロセスはそのボックスから1つのメッセージを選択して処理し、適切と思われる順序で次のメッセージに移動します。これは非常に順次的な行為であり、受信ブロックはまさにそれを行います。
Chrisが言ったように、同期と順次を混同しているようです。