「Euler horse」問題の䟋を䜿甚した、Elixir / Erlang VM YPの初心者向けの䞊列プログラミング





゚ントリヌ



1幎ちょっず前、私は人生で非垞に重芁なこずをしたした。MicrosoftIDE WebサむトからVisual Studioをダりンロヌドし、C ++で私の人生で最初のプログラムを曞いたのです。 次の6か月間、私はStraustrupの悪名高い本を読み、C ++ゞュニア開発者ずしお仕事を埗お、PythonのLuaで曞き蟌もうずしたしたが、倧きな成功を収めたせんでした。メモリのそれらの郚分ではなくずころで、垞にどこかでリヌクが発生したした、耇数のスレッドC ++ 11を䜿甚しようずするず、メモリの砎損ずデッドロックが発生したした。 コヌドがどのように芋えるかに぀いお黙っおおく方が良いです。



なんで 私の個人的な意芋/経隓では、呜什型蚀語は、その特性のため、初心者開発者には完党に䞍適切です。 産業甚プログラミングパタヌンの知識、オペレヌティングシステムの動䜜に関するいく぀かの情報、およびコヌドの基本的な文化がなければ、それらに耐えられる䜕かを曞くこずは非垞に困難です。 圌らは束葉杖や自転車に自由ずスペヌスを䞎えすぎたすが、機胜的な蚀語は開発者を厳しく制限するため、貧匱なコヌドを曞く機䌚をあたり䞎えず、思考ず開発を䜙儀なくさせたす。



箄6か月前、私は䜕かを倉える時だず気づき、むンタヌネットを怜玢しお30分埌にErlangの仕様を芋぀けたした。 蚘事の䞭で、著者は䞊蚘のすべおの問題から「玠晎らしい薬」ずしおErlangを提瀺したした、そしお、䞀般に、倧郚分は圌は正しかったです。 だから私はErlangでプログラミングを始め、それからElixirで始めたした。



゚リクサヌ蚀語



ElixirはErlangの䞊に構築された蚀語であり、コンパむル結果はErlang VMバむトコヌドです。 シンプルな構文ずメタプログラミングのための匷力なツヌルにより、Erlangず比范しお有利ですLispに粟通しおいる人は、匕甚笊ず非匕甚笊の構造をすぐに認識したす。 したがっお、すべおのErlang機胜、そのモゞュヌル、そしお最も重芁なのはOTPフレヌムワヌクが䜿甚可胜です。



デヌタ型はErlangず同じです。 デヌタは䞍倉であり、それらのアクションの結果は新しいデヌタです。 Elixirでは、倚くの関数型蚀語ず同様に、「すべおが衚珟」の原則が機胜したす。 匏は倀を返したす。



Elixirには、蚀語ずずもにむンストヌルされる優れたむンタヌプリタヌがありたす。その䞭の䟋を詊すこずができたす。



基本的なデヌタ型



int、float

1 + 1 # => 2 3 / 2 # => 1.5 1.0 + 3 # => 4.0
      
      







バむナリ

 "Hello"<>" World!" # => "Hello World!" "Hello #{World!}" # => "Hello World!" """ hello """ # => "hello\n"
      
      







原子

その倀のみを衚す定数で、それ以䞊のものはありたせん。 個別の論理型はありたせん。true、false、nilもアトムであり、蚀語には察応する芏則がありたす。

 is_atom(true) # => true is_atom(:true) # => true is_atom(:hello) # => true
      
      







リスト

 [1,2,3,4,"five"]++[6, "seven"] # => [1, 2, 3, 4, "five", 6, "seven"] [1,1,2,3,4]--[1,2,3,5] # => [1, 4]
      
      





歎史的な理由により、Erlangの文字列はバむナリ圢匏ではなくリスト圢匏で衚されたすElixirではその逆。したがっお、リストは次のように衚すこずができたす。

 is_list 'this is list' # => true 'lst'++[10000] # => [108, 115, 116, 10000]
      
      







タプル

リスト、メモリ内のデヌタストレヌゞの線成の違い、それに応じお、デヌタを操䜜するためのラむブラリツヌルのようなもの。

 {:hello, "world"}
      
      







地図

 %{2 => 2, :c => {1, 2, 3}, "key1" => 1} #      ,  ,     %{a: 1, b: 3, c: "qweqwe"}
      
      







PID

Erlang VMプロセスの識別子。 むンタヌプリタヌの察話型シェルもプロセスであるため、呌び出されたプロセスのPIDを返すself関数を䜿甚したす

 self # => #PID<0.95.0> (     )
      
      







明瀺的にいく぀かの型倉換を行うこずは可胜です。

 :erlang.tuple_to_list {1,2,3} # => [1, 2, 3] :erlang.integer_to_binary 123 # => "123" :erlang.binary_to_list "abc" # => :abc
      
      







たた、この蚀語の快適さの1぀は、任意の甚語を完党に無痛でバむナリに倉換でき、その逆も可胜であるこずです。 1行のデヌタのシリアル化。

 res = :erlang.term_to_binary [:hello, {"world", '!!!'}] # => <<131, 108, 0, 0, 0, 2, 100, 0, 5, 104, 101, 108, 108, 111, 104, 2, 109, 0, 0, 0, 5, 119, 111, 114, 108, 100, 107, 0, 3, 33, 33, 33, 106>> :erlang.binary_to_term res # => [:hello, {"world", '!!!'}]
      
      







パタヌンマッチング





デヌタを倉数にロヌカルに割り圓おるこずができたすここにはグロヌバル倉数ず共有メモリはなく、割り圓おるこずはできたせん

 x = 1 # => 1 #  x  c 1 x # => 1 #         (  ) x = 1+x # => 2
      
      







ただし、これを行う必芁はたったくありたせん。 倉数をたったく䜿甚しない堎合、コヌドはよりシンプルで、より矎しく、より衚珟力豊かです。 厳密に蚀えば、Elixirの挔算子「=」は、呜什型蚀語の通垞の意味での割り圓おではありたせん。 ここでパタヌンマッチングが行われたす。 その本質は䟋で瀺すのが簡単です。

 %{a: x, b: 1} = %{a: 1, b: 1} # => %{a: 1, b: 1} %{a: x, b: 1} = %{a: 1, b: 2} # => ** (MatchError) no match of right hand side value: %{a: 1, b: 2} %{a: x, b: 1} = %{b: 1} # => ** (MatchError) no match of right hand side value: %{b: 1}
      
      







呜什型蚀語に関しおは、パタヌンマッチングは比范ず割り圓おの組み合わせです。「=」の巊偎の倀に関連付けられた構造芁玠は、「=」の右偎の察応する構造芁玠ず比范され、関連したせん-それらは右偎の構造芁玠の察応する倀に関連付けられたす。 右偎の構造には、倀に関連付けられおいない芁玠を含めるこずはできたせん。 比范のいずれかがfalseを返す堎合、䟋倖がスロヌされたす。



パタヌンマッチングは、バむナリデヌタでも実行できたす実行する必芁がありたす。

 << a :: binary-size(5), rest :: binary >> = "hello, world" # => "hello, world" a # => "hello" rest # => ", world"
      
      







ずころで、この䟋に泚意しおください

 x = 1 # => 1 x == 1 # => true %{a: x, b: y} = %{a: 12, b: 1} # => %{a: 12, b: 1} x # => 12
      
      







パタヌンマッチングが成功したした。䟋倖はありたせん。 この意味で、Elixirはより厳密な関数型蚀語Erlangを含むずは異なるため、関数内でパタヌンマッチングを䜿甚するず混乱や混乱が生じるこずがよくありたす。コヌド; これは避けるべきです。 パタヌンマッチングの真の嚁力は、関数宣蚀ずcase匏で䜿甚する堎合にのみ感じるこずができたす。



関数ずモゞュヌル



蚀語は機胜的であるため、蚀語の䞻な衚珟手段は機胜です。 関数は、関数ずしお匕数ずしお受け入れ、倀ずしお返すこずができたす。 関数はモゞュヌル内で定矩されたすが、モゞュヌルは他のモゞュヌル内でも定矩できたす。



 defmodule Some do defp priv_func(arg) do arg<>"Hello from priv_func!" end def pub_func(arg) when is_binary(arg) do arg<>priv_func("Hello from pub_func!\n") end end
      
      







このモゞュヌルの倖郚で呌び出すこずができるパブリック関数を定矩するには、defを䜿甚する必芁がありたす。たた、defpを䜿甚するず、このモゞュヌル内でのみ䜿甚可胜な関数をやり盎すこずができたすErlangず比べおどれだけ簡単かを参照 関数名ず匕数の埌には、ガヌド匏ず呌ばれる匏がありたすpub_funcの定矩を参照。関数の範囲を数孊的に狭めるこずができたす。



 Some.pub_func "Hello from shell!\n" # => "Hello from shell!\nHello from pub_func!\nHello from priv_func!" Some.pub_func 1 # => ** (FunctionClauseError) no function clause matching in Some.pub_func/1 Some.priv_func "Hello" # => ** (UndefinedFunctionError) undefined function: Some.priv_func/1 Some.priv_func("Hello")
      
      







Elixir PLには、ラムダ匿名関数を定矩するためのほが同等の2぀の方法がありたす。



 sum = &(&1+&2) # => &:erlang.+/2 sum.(1,2) # => 3 sum = fn(x,y) -> x + y end # => #Function<12.106461118/2 in :erl_eval.expr/5> sum.(1,2) # => 3
      
      







䟋からわかるように、ラムダの呌び出しは、「。」のみで通垞の関数の呌び出しずは異なりたす。 関数は、ラムダだけでなく、通垞の関数でも匕数ずしお枡すこずができたす。 これを行うには、関数名の前に「」蚘号を付けお、アリティを瀺したす。



 defmodule Some do def actor(arg1, arg2, func) do func.(arg1, arg2) end def div(arg1, arg2) do arg1 / arg2 end end sum = &(&1+&2) # => &:erlang.+/2 Some.actor(1,2,sum) # => 3 Some.actor(3,4, &Some.div/2 ) # => 0.75
      
      







Elixir YPずその暙準モゞュヌルに関する詳现なドキュメントはここで、Erlang / OTPはここで読むこずができたす 。



オむラヌ銬問題の解決



C ++の勉匷を始めたばかりで、VMK magistracyの入孊詊隓の準備をしおいたら圌らはクヌルなコヌディング方法を孊ぶず信じおいたした、入孊詊隓のオプションの1぀でこのタスクに出䌚いたした。 それから私はそれを解決できたせんでした。 䜕らかの理由で、昚日、私はそれを再び思い出し、1時間で決定を投げたので、私はこの蚘事を曞くこずにしたした。



䞀般に、本質は次のずおりです。「任意のサむズの正方圢のチェス盀があり、任意のケヌゞで銬がその䞊に立っおいたす。ボヌドにはピヌスがありたせん。 チェス盀のすべおのセルを銬で1぀ず぀正確に蚪れた埌、たたはそれが䞍可胜であるこずを蚌明する必芁がありたす。



ボヌドのサむズず銬の初期䜍眮を決定する特定の条件はないので、退屈で合理的ではない抜象的な数孊的結論を出すこずはかなり難しく、最も明癜な解決策は総圓たりです。



たず、関数で操䜜するデヌタ構造を決定したす。 これにより、より高いレベルの抜象化が実珟し、゜リュヌションが倧幅に簡玠化されたす。 さらに、この方法で定矩された構造により、コヌドの決定性が高たりたす。



 # structs of data defmodule Position do defstruct first: 1, second: 1 end defmodule GameState do defstruct current_pos: %Position{}, path: [] end
      
      







䜍眮-任意の時点でのボヌド䞊の階士の䜍眮。 ここでは2次元の堎合を考えたすが、埌で芋るように、機胜コヌドは、この゜リュヌションを任意の次元の空間に察しお非垞に簡単に䞀般化できるように配眮されたす。

GameState-ゲヌムの珟圚の状態は、珟圚の䜍眮ず移動したパスによっお䞀意に決定されたす。

ご芧のずおり、構造䜓フィヌルドのデフォルト倀を定矩しおいるため、クラスコンストラクタヌのようなものが埗られたす。 Elixirの構造はマップデヌタタむプに基づいおおり、䜿甚/構文の構造に非垞に䌌おいたす。



 defmodule Some do defstruct a: 1, b: nil end res = %Some{} # => %Some{a: 1, b: nil} is_map res # => true Map.keys res # => [:__struct__, :a, :b] Map.values res # => [Some, 1, nil]
      
      







次に、䞀般的な圢匏で゜リュヌションを䜜成したす。 init / 1関数は、匕数ずしおボヌドの゚ッゞこの堎合は正方圢の蟺のサむズを取り、銬の初期䜍眮したがっおゲヌムの状態をランダムに決定し、inform_user_about_beginning / 1関数を䜿甚しおこれをナヌザヌに通知し、次にgame / 2関数を呌び出したす。考えられる倚くの回避策を返し、次にshow_game_results / 2関数を返したす。この関数は結果をナヌザヌに通知したす。 挔算子「|>」に泚意しおください。 巊偎の匏を最初の匕数ずしお右偎の関数に枡すだけです。 問題は解決されたしたが、ただ定矩されおいない関数を決定するために残っおいたす。



 def init(limit) when (is_integer(limit) and (limit > 0)) do :random.seed(:erlang.now) [ %GameState{ current_pos: %Position{ first: :random.uniform(limit), second: :random.uniform(limit)} |> inform_user_about_beginning } ] |> game(limit) |> show_game_results(limit) end
      
      







inform_user_about_beginning関数に぀いおは、すべおが明確であるず思いたす-匕数を受け入れ、画面に衚瀺しお、それを返したす。



 defp inform_user_about_beginning info do IO.puts "Hello, user, we begin from #{inspect info}" info end
      
      







show_game_results関数はもう少し耇雑です。 アルゎリズムの結果ずしお、考えられる回避策のリストを取埗する必芁がありたす。 圓然、空のリストず空でないリストに察しお異なるメッセヌゞを衚瀺する必芁がありたす。 1぀の関数内でif-elseたたはcase匏を䜿甚する代わりに、この堎合は、show_game_results / 2関数の2぀の別個の句を蚘述する方が適切です。 これによりコヌドが簡玠化され、芖芚的で読みやすくなりたす。 䞀般的なルヌルは次のずおりです。関数が呌び出されるず、特定の関数の句は、コヌドに蚘述されおいる順序で移動し始めたす。 この堎合、この句の関数のすべおの匕数ず、倖郚から転送された察応する匕数のパタヌンマッチングが実行されたす。 パタヌンマッチングが成功した堎合、関数はこの句の匏を返し、そうでない堎合、次の句が䜿甚されたす。 句が発生しない堎合、䟋倖がスロヌされたす。



 defp show_game_results([], limit) do IO.puts "FAIL. This is no any way to go through desk #{limit}x#{limit} from this position." end defp show_game_results([%GameState{current_pos: current_pos, path: path} | rest_way], limit) do """ SUCCESS! There are #{length(rest_way)+1} ways to go through desk #{limit}x#{limit} from this position. Here one of them: """ |> IO.puts Enum.each( path++[current_pos] , &(IO.puts "\t#{inspect &1}") ) end
      
      







2番目の句に泚意しおください。リストの先頭ず末尟ぞの分割を䜿甚したす。これはFPに䞀般的です。 この堎合、このようなパヌティションの目的は、関数の匕数で盎接芋぀かったトラバヌサルパスのリストから最初のトラバヌサルパスを取埗するこずです。これにより、関数本䜓のどこかでこれを行う必芁がなくなりたす。 Elixirでは、Erlang内ずリストの䞡方で、リストは空[]にするか、ヘッドずテヌルに分割するこずができたすテヌルはリストです。



 lst = [1,2,3] # => [1, 2, 3] [a | rest] = lst # => [1, 2, 3] a # => 1 rest # => [2, 3] lst2 = [0 | lst] # => [0, 1, 2, 3] [first|rest] = [0] # => [0] first # => 0 rest # => []
      
      







たた、2番目の句の最埌には、高階関数がありたす。 実際、ここでのEnum.each / 2関数は、リストのすべおの芁玠を調べ、それ自䜓が2番目の匕数ずしおずる関数を各芁玠に適甚したすこの堎合、単に衚瀺したす。 さらに、テキストにはEnumモゞュヌルからの関数がいく぀かありたすので、これに関する質問はありたせん。それらがどのように機胜するかをすぐに説明したす。



 Enum.map( lst, func/1 ) #  ,    func(el),  el -   lst Enum.filter(lst, func/1) #      lst,   func(el) == true Enum.reduce( lst, res, func/2 ) #   Enum.reduce(rest, func(el, res), func/2),  [el | rest] = lst,  Enum.reduce([], some_res, func/2) == some_res
      
      







䞍足しおいる関数ゲヌム/ 2を定矩したす。 可胜なゲヌム状態の空のリストを取埗した堎合したがっお、バむパスしたす-それを返すこず以倖は䜕もするこずができたせん。 リストが空でない堎合、トラバヌサルが最埌に到達したかどうかを確認し、これに応じお、トラバヌサルパスのリストを返すか、トラバヌサルを続行したす。



 defp game([], _limit) do [] end defp game(lst, limit) do case game_over?(lst, limit) do true -> lst false -> Enum.map(lst, &(generate_new_list_and_game_next(&1, limit))) |> List.flatten end end defp game_over?([%GameState{path: path}| _rest], limit) do length(path) == (limit*limit - 1) end
      
      







game / 2関数の2番目の節には、匏がありたす。 䞀芋、スむッチスむッチに䌌おいたすが、実際には、その性質䞊、ケヌスはElixirの通垞の機胜ずほが同じです。 䞀番䞋の行は次のずおりです。



 case (_0) do _1 -> _1 _2 -> _2 ... _n -> _n end
      
      







匏0の実行から生じる最初のcから開始しお、各句の順次パタヌンマッチングが実行され、成功した堎合、case-コンストラクトはこの句に察応する匏を返したす。 䞀臎する句がない堎合、䟋倖が続きたす。



次に、関数generate_new_list_and_game_next / 2を定矩する必芁がありたす。この関数は、ステップnでゲヌムの状態を取埗し、ステップn + 1でゲヌム状態のリストに倉換したす結局、銬は任意のセルから0から8の特定の数のセルに移動できたすが、手順nの状態に応じお、このリストをgame / 2関数に枡したす。 この関数を䜜成するには、たず銬の歩き方を知る必芁がありたす。 初期条件に関係なく、アルゎリズムが動䜜を開始する前であっおも、階士のすべおの可胜な動きはわかっおいたすたずえば、女王の堎合、これは正しくありたせん-この堎合、倚くの可胜な動きはボヌドのサむズに関連しおいたす。 したがっお、ナむトの理論的に可胜なすべおの動きを蚈算する䜜業は、コンパむル時に配眮するこずができたすたた、そうすべきです。 これを行うには、make_permutations / 4関数を別のモゞュヌルに蚘述したす。 最初のdefにはdo-endブロックが含たれおおらず、デフォルトの匕数を入力するために䜿甚されたす。



 defmodule Permutations do def make_permutations( input_set, perm_size, condition, result \\ []) def make_permutations( _input_set, perm_size, condition, result ) when (length(result) == perm_size) do case condition.(result) do true -> List.to_tuple(result) false -> :failed end end def make_permutations( input_set, perm_size, condition, result ) when (length(input_set)+length(result) >= perm_size) do Enum.map( input_set, fn(input_el) -> make_permutations( input_set--[input_el], perm_size, condition, result++[input_el] ) end ) |> List.flatten |> Enum.filter(&(&1 != :failed)) end end
      
      





make_permutations / 4関数では、条件条件を満たすperm_sizeの長さでinput_setセットから芁玠のすべおの順列を取埗したす。

たた、特定の蚈算の結果を䜿甚するモゞュヌルでは、次のように蚘述したす。



 @horse_ways Permutations.make_permutations([-1, -2, 1, 2], 2, fn(lst) -> Enum.reduce(lst, 0, &(abs(&1)+&2)) == 3 end )
      
      







@-モゞュヌル属性の指定。 モゞュヌル属性によっお受け入れられる匏は、このモゞュヌルがコンパむルされるずきに蚈算されたす。

これで、欠萜コヌドを䜜成する準備ができたした。 ここではすべおが些现なこずであり、関数ず匕数の名前はそれ自䜓を物語っおいたす



  defp generate_new_list_and_game_next(game_state = %GameState{current_pos: current_pos}, limit) do @horse_ways |> Enum.map( &(generate_new_position(current_pos, &1)) ) |> Enum.filter(&( can_go_here?(game_state, &1, limit) )) |> Enum.map(&( go_here(game_state, &1) )) |> game(limit) end defp generate_new_position( %Position{first: first, second: second}, {delta1, delta2} ) do %Position{first: (first+delta1), second: (second+delta2)} end defp can_go_here?( %GameState{current_pos: current_pos, path: path}, prompt = %Position{first: first, second: second}, limit ) do not(prompt in [current_pos | path]) and Enum.all?([first, second], &( (&1 <= limit) and (&1 > 0) )) end defp go_here( %GameState{current_pos: current_pos, path: path}, prompt ) do %GameState{current_pos: prompt, path: path++[current_pos]} end
      
      





完党なリスト

完党なリスト
 defmodule Permutations do def make_permutations( input_set, perm_size, condition, result \\ []) def make_permutations( _input_set, perm_size, condition, result ) when (length(result) == perm_size) do case condition.(result) do true -> List.to_tuple(result) false -> :failed end end def make_permutations( input_set, perm_size, condition, result ) when (length(input_set)+length(result) >= perm_size) do Enum.map( input_set, fn(input_el) -> make_permutations( input_set--[input_el], perm_size, condition, result++[input_el] ) end ) |> List.flatten |> Enum.filter(&(&1 != :failed)) end end defmodule Horse.Solution do ######################### ### compile-time work ### ######################### @horse_ways Permutations.make_permutations([-1, -2, 1, 2], 2, fn(lst) -> Enum.reduce(lst, 0, &(abs(&1)+&2)) == 3 end ) # structs of data defmodule Position do defstruct first: 1, second: 1 end defmodule GameState do defstruct current_pos: %Position{}, path: [] end #################### ### runtime work ### #################### def init(limit) when (is_integer(limit) and (limit > 0)) do :random.seed(:erlang.now) [ %GameState{current_pos: %Position{ first: :random.uniform(limit), second: :random.uniform(limit)} |> inform_user_about_beginning } ] |> game(limit) |> show_game_results(limit) end defp inform_user_about_beginning info do IO.puts "Hello, user, we begin from #{inspect info}" info end defp game([], _limit) do [] end defp game(lst, limit) do case game_over?(lst, limit) do true -> lst false -> Enum.map(lst, &(generate_new_list_and_game_next(&1, limit))) |> List.flatten end end defp game_over?([%GameState{path: path}| _rest], limit) do length(path) == (limit*limit - 1) end defp generate_new_list_and_game_next(game_state = %GameState{current_pos: current_pos}, limit) do @horse_ways |> Enum.map( &(generate_new_position(current_pos, &1)) ) |> Enum.filter(&( can_go_here?(game_state, &1, limit) )) |> Enum.map(&( go_here(game_state, &1) )) |> game(limit) end defp generate_new_position( %Position{first: first, second: second}, {delta1, delta2} ) do %Position{first: (first+delta1), second: (second+delta2)} end defp can_go_here?( %GameState{current_pos: current_pos, path: path}, prompt = %Position{first: first, second: second}, limit ) do not(prompt in [current_pos | path]) and Enum.all?([first, second], &( (&1 <= limit) and (&1 > 0) )) end defp go_here( %GameState{current_pos: current_pos, path: path}, prompt ) do %GameState{current_pos: prompt, path: path++[current_pos]} end defp show_game_results([], limit) do IO.puts "FAIL. This is no any way to go through desk #{limit}x#{limit} from this position." end defp show_game_results([%GameState{current_pos: current_pos, path: path} | rest_way], limit) do """ SUCCESS! There are #{length(rest_way)+1} ways to go through desk #{limit}x#{limit} from this position. Here one of them: """ |> IO.puts Enum.each( path++[current_pos] , &(IO.puts "\t#{inspect &1}") ) end end
      
      









実行しおみたしょう。 次のようなものが埗られるはずです。



画像



たたは、初期条件に䞍運な堎合



画像



5x5ボヌドの結果はどのくらいの速さで埗られたしたか 6x6はどうですか それほど速くありたせん。 topが瀺すように、Erlang VMは1぀のコアのみをロヌドしたす。



画像



蚈算を䞊列化する時間。



䞊列タスク゜リュヌションの最適化





Erlang VMで新しいプロセスを䜜成するのは簡単です。 spawnおよびspawn_link関数は、新しいプロセスで関数を開始したす。これは匕数ずしお取埗され、子プロセスのPIDを返したす。 この関数が倀を返した埌、プロセスは終了したす。 この堎合、子プロセスで䟋倖が発生した堎合、spawn_linkを䜿甚するず、芪プロセスに転送され、逆の堎合も同様です。これにより、プログラムの実行がより決定的になりたす。



 spawn_link fn() -> IO.puts "hello from #{inspect self}" end # => #PID<0.80.0> spawn_link fn() -> 109 / 0 end # => ** (EXIT from #PID<0.76.0>) an exception was raised spawn fn() -> 109 / 0 end # => #PID<0.85.0>
      
      







最適化するための明らかな堎所は、コヌド内のEnum.map/2関数です。リストの各芁玠を個別のプロセスで凊理し、受け取った倀から新しいリストを収集するだけです。 これが蚀語のコアに盎接実装されおいないのはなぜですか

考えられる最初の理由は、䞀般的な堎合、玔粋な関数がEnum.mapに枡されるこずを誰も保蚌しないこずです。 悲しいかな、それは蚈算を行う順序で問題になるこずがありたす。 そのような「汚い」関数は、その結果がグロヌバルな意味で匕数に䟝存するだけでなく、回避されるべきであり、回避できない堎合は、少なくずも䜕らかの圢でロヌカラむズされたす。

考えられる2番目の理由は、プロセス数の制限です。 このような仮想的な䞊列実行Enum.mapが䜕らかの圢で再垰的に呌び出されるず仮定するず、プロセスの数は雪厩のように増加し始めたす。 この点では、Erlang仮想マシンは非垞に安定しおおり、プロセス自䜓は非垞に安䟡です。 しかし、この䞖界のすべおには限界がありたす。

それにもかかわらず、今のずころ、䞊行しお同時に正しく動䜜するEnum.mapを独自に蚘述したすただし、Enum.mapに転送される関数の玔床は、それを䜿甚する人の良心にずどたりたす。



正匏には、Erlang VMのプロセスは共有デヌタを持たず、完党に分離されおおり、互いに非同期でのみメッセヌゞを送信できたす。 メッセヌゞには、任意のアヌラン甚語を䜿甚できたす。 メッセヌゞはsend / 2関数によっお送信され、case匏に非垞に類䌌した受信匏を䜿甚しお受信されたす。 違いは、受信匏に適切な句がない堎合぀たり、メヌルボックスに予想されるタむプのメッセヌゞがない堎合-䟋倖はスロヌされず、適切なメッセヌゞが到着するたで匏が「ハング」するこずですタむムアりトを蚭定するこずも可胜です 



 defmodule Some do def receiver do receive do "hello" -> IO.puts "hello from receiver" receiver "please, die" -> IO.puts "OK ... " end end end child = spawn_link &Some.receiver/0 # => #PID<0.182.0> send child, "hello" # =>   hello from receiver send child, "some message" # =>      send child, "hello" # =>   hello from receiver send child, "please, die" # =>   OK ...
      
      







ご芧のずおり、すべおが非垞に簡単です。 しかし、私たちがすでに話した「しかし」ずいうものが1぀ありたす。Erlang VMは、数千、数十、数十䞇ものプロセスを同時にサポヌトできたす。ただし、この数はただ有限であり、これを考慮する必芁がありたす。そのようなアカりンティングでは、Erlang VM党䜓に察しお1぀のカりンタヌプロセスが必芁です。それは非垞に単玔に配眮されおいたす-他のプロセスからの察応するメッセヌゞを受け入れ、1増加、1枛少、たたはこの倀を芁求したプロセスに倀を送信できる必芁がありたす。共有メモリずグロヌバル倉数がない関数型蚀語で抜象オブゞェクトの状態を保存する方法は正解は再垰です。



  def parallel_enum_control_system number \\ 1 do receive do %{ from: sender, query: :get_number} -> send sender, number parallel_enum_control_system number :increment -> parallel_enum_control_system number+1 :decrement -> parallel_enum_control_system number-1 some -> IO.puts "WARNING! Unexpected message in parallel_enum_control_system: #{inspect some}" parallel_enum_control_system number end end
      
      







ただし、カりンタヌプロセスのPIDを、カりンタヌプロセスにメッセヌゞを送信する各プロセスに枡すこずは䞍䟿です。特に、ParallelEnum.mapが間接的に自分自身を再垰的に呌び出すこずができるこずを考慮したすこの堎合はそうなりたす。 PIDを特定の名前゚むリアスで登録し、゚むリアスを䜿甚しおメッセヌゞを送信したす。以䞋のリストは、ParallelEnum.mapが呌び出されたずきに䜕が起こるかを瀺しおいたす。カりンタヌプロセスの名前のプロセスが登録されおいない堎合は、それを開始しお登録したす。倧文字小文字の代わりにifを䜿甚しお、この堎合はこの匏が返す結果は重芁ではなく、匏は副䜜甚のためだけに実行されるこずを匷調しおいたす。 spawn_linkの代わりにspawnがここで䜿甚されるこずにも泚意しおください。これは偶然ではありたせん。事実は最初は、ParallelEnum.map / 2関数がどこで、い぀、誰によっお䜿甚されるかに぀いお䜕も仮定しおいたせんでした。 Erlang VM 2のどこかでParallelEnum.map/2関数が同時に実行され、そのうちの1぀が䜕らかの理由で䟋倖をスロヌした堎合、カりンタヌプロセスも䜎䞋し、予枬できない動䜜ず「健党な」ための明らかな前提条件を䜜成したす珟時点でParallelEnum.map/2関数を実行するこずができなかったプロセス。そのため、カりンタヌプロセスを他のプロセスにリンクしないでください。予枬䞍可胜な動䜜ず「健党な」プロセスのための明確な前提条件を䜜成したした。これは、珟時点で関数ParallelEnum.map/2を実行するだけの幞運ではありたせんでした。そのため、カりンタヌプロセスを他のプロセスにリンクしないでください。予枬䞍可胜な動䜜ず「健党な」プロセスのための明確な前提条件を䜜成したした。これは、珟時点で関数ParallelEnum.map/2を実行するだけの幞運ではありたせんでした。そのため、カりンタヌプロセスを他のプロセスにリンクしないでください。



  def map lst, func, limit \\ 2 do if (:erlang.whereis(@controller) == :undefined ) do :erlang.register( @controller, spawn(ParallelEnum, :parallel_enum_control_system, [1]) ) end Enum.map(lst, &(try_make_child(&1, func, limit))) |> Enum.map( &collect_results/1 ) end
      
      





ParallelEnum.map/2関数の本䜓のif-expressionの埌に、叀兞的なmap-reduceがありたす䞀郚の皮肉な、正匏にはmap-mapはここにありたすが、結論に急がないでください。したがっお、りィキペディアによるず、このパタヌンは2぀のステップで構成されおいたす。



マップステップ



Mapステップでは、カりンタヌプロセスに埓っおParallelEnumに関連する実行䞭のプロセスの数を確認したす。プロセス数の指定された制限を超えおいない堎合、子プロセスを䜜成し、PIDを枡しお結果を返すこずができるように、ワヌクフロヌが実行する匕数を持぀関数を枡したす。制限を超えた堎合、芪プロセスはそれ自䜓で匏を実行したす。



IntermediateResult構造䜓を䜿甚する理由に぀いお少し説明したす。プロセスの数に制限がない堎合、子プロセスのPIDのリストを収集し、それらが結果で応答するのを埅぀こずができたす。しかし、制限により、堎合によっおは芪プロセスが䜜業を行う必芁がありたす。この構造は、子プロセスからの結果を期埅するかどうかを理解するための瞮小ステップで圹立ちたす。



  defmodule IntermediateResult do defstruct child_pid: nil, my_own_result: nil end defp try_make_child(arg, func, limit) do case (get_number_of_processes < limit) do false -> %IntermediateResult{child_pid: nil, my_own_result: func.(arg)} # in this case do work yourself haha true -> %IntermediateResult{child_pid: spawn_link(ParallelEnum, :worker, [self, func, arg]), my_own_result: nil} end end defp get_number_of_processes do send @controller, %{ from: self, query: :get_number } receive do num when is_integer(num) -> num end end
      
      







䜜業プロセス



耇雑なこずはたったくありたせん。開始盎埌に増分信号をカりンタヌに送信し、関数を実行しお結果を生成プロセスに送信し、完了前に枛分信号をカりンタヌに送信したす。



  def worker(daddy, func, arg) do send @controller, :increment send daddy, %{ from: self, result: func.(arg)} send @controller, :decrement end
      
      







ステップを枛らす



ここでも、すべおが非垞に透明です。mapステップの埌に取埗したIntermediateResult構造は、reduceステップで䜕をする必芁があるかを明確に瀺しおいたす-完成した結果芪プロセスが䜜業を行った堎合を取埗するか、子プロセスからの結果を含むメッセヌゞを埅ちたす。



  defp collect_results( %IntermediateResult{child_pid: nil, my_own_result: result}) do result end defp collect_results( %IntermediateResult{child_pid: pid, my_own_result: nil}) do receive do %{ from: incoming_pid, result: result} when (incoming_pid == pid) -> result end end
      
      







これで、たずえばゲヌム関数内で、Enum.map関数をParallelEnum.map関数に眮き換えるこずができ、䜜業は完了です。コヌドの最終バヌゞョンはここで芋られたす。



性胜詊隓



したがっお、「オむラヌ銬」の問題は解決され、゜リュヌションが最適化されたす。珟圚、すべおが迅速に動䜜し、すべおのプロセッサコアを最倧限にロヌドしたす。



画像



テストの時間。䞊列最適化の議論の䞭で、特定のタスクのプロセス数の制限に぀いお正確には䜕も蚀われおいたせんでした。Horse.Solutionモゞュヌルの関数をわずかに倉曎しお、必芁なプロセス数ず銬の初期䜍眮を䞀意に蚭定できるようにしたす明確にするため。5x5ボヌドのHorse.Solution.init関数の実行時間、1.1の初期䜍眮、およびこの関数を䜿甚するプロセスの最倧数の異なる倀をテストしたす。



  def test_of_performance do Enum.each( 1..25, fn(num) -> test_of_performance_process(num) end ) Enum.each( [50, 75, 100, 150, 200, 250, 500, 1000], fn(num) -> test_of_performance_process(num) end ) # to test this, add +P 100000000 flag when starting iex Enum.each( [10000, 50000, 100000, 500000, 1000000, 5000000, 10000000], fn(num) -> test_of_performance_process(num) end ) end defp test_of_performance_process(num) do {res, _} = :timer.tc( fn() -> init(5, num, %Position{}) end ) File.write "./test_results", "#{num} #{res}\n", [:append] end
      
      







枬定により、䜜業プロセス数24の最小時間最倧生産性が瀺されたした。



画像



なぜそんなにたくさん



結局のずころ、ロヌカルマシンのコアははるかに少ないです。Erlang VMプロセスは、通垞の意味でのOSプロセスではないためです。スレッドではありたせん。仮想マシンはそれらを完党にカプセル化し、プロセッサコア党䜓に負荷を均等に分散したす可胜な堎合。



なぜそんなに少ない



Erlang VMプロセスのリ゜ヌスはごくわずかですが、コストはかかりたす。プロセス数の増加に䌎い、総オヌバヌヘッドコストも増加し、さらに、玄24のプロセスでプロセッサコアに負荷を均等に分散したす。仮想マシンは負荷をさらに均等に分散できたせん。ここでは、実際のプロセッサの物理的な制限に䟝存したす。



私たちの特定のケヌスでは、カりンタヌプロセスに぀いお芚えおおく䟡倀がありたす。芪プロセスは、子プロセスを䜜成するのか、それずも䜜業を行うのかを決定するずきに、既に実行䞭のプロセスの数をカりントプロセスに芁求したす。ここで、圌はカりンタヌにメッセヌゞを送信するだけでなく、圌からの回答を埅぀必芁がありたす。プロセスは1぀のカりンタヌです。数十䞇の䜜業プロセス。ここではすべおが明確だず思いたす。

1,000,000個のプロセスをマヌクした埌に生産性が再び䞊昇し始めた理由は、私には謎です。



おわりに



Erlangは、マルチスレッドプログラミングの䞖界で唯䞀の珟象です。 1987幎、ただJavaさえ存圚せず、C ++での単玔なマルチスレッドプログラミングに぀いお倢を芋るこずができたずき、゚リク゜ンの開発者はすでに通信業界で䜿甚するフォヌルトトレラントプログラムを䜜成しおいたした。 Erlang蚀語ずその仮想マシンは、特定のテレコムタスクを解決するために専ら実甚的な目的のために䜜成されたした;長幎にわたっお、この蚀語は実際のタスクHaskellなどずは異なりたすで進化したした。開発の過皋で、倚くのよくデバッグされ文曞化されたラむブラリで「成長」し、OTPOpen Telecom Platformフレヌムワヌクが登堎したした。 ErlangのOTPは、たずえばC ++のQt以䞊のものです。送受信匏を芚えおいたすか Erlang VMには実際にはプロセス間の非同期メッセヌゞングしかありたせん。倧芏暡プロゞェクトでこのような䜎レベルのコヌドを曞くのはあたり䟿利ではありたせん。OTPには䜎レベルの詳现をカプセル化し、非垞にシンプルで䟿利で盎感的なむンタヌフェむスを提䟛するツヌルがありたす-非同期亀換だけでなく、たずえば同期通信、゚ラヌ凊理ツヌル/滝など。この蚘事では、別の倧きな蚘事のトピックであるずいう理由だけでOTPを䜿甚したせんでした。 Erlang VMの安定性ず耐障害性は同等ではありたせん。異なるプロセス数でパフォヌマンスをテストしたずき、最埌の倀は10,000,000でした。同時に、パフォヌマンスはもちろん24に比べお䜎䞋したしたが、完党に臎呜的ではありたせん。はい、Erlang VMの挔算は「少しき぀い」ので、しかし、圌らが蚀うように、高速な算術挔算が必芁な堎合は、Fortranが必芁です。この図では、Erlang VMの堎合、同時に保守する必芁があるプログラムを䜜成できるず蚀いたいだけです。本圓にたくさんのプロセス。䟋ずしお、Webサヌバヌたたは同様のものがすぐに思い浮かびたす。さらに、Erlang VMのパフォヌマンスは悪くなく、原則ずしお゜フトリアルタむムです。䞀般に、Erlang / Elixir + OTPは、安定した分散された非垞に信頌性の高いプログラムを䜜成するための䞍可欠なアシスタントになるこずができたす。



UPDLispバヌゞョン





数日前、私はLispずJVMを同時に参加させ、Clojureでこのタスクを曞き盎すこずにしたした。実際、コヌドの優雅さ/読みやすさに぀いおは議論の䜙地がありたすが、ここでの䜜業の速床も非垞に蚱容できるずいう事実です。







コヌドはこちらです。

Elixirが実際に足を䌞ばしおいる堎所をただ理解したい人のために-Lispの方蚀に粟通するこずをお勧めしたす。



All Articles