エラーとプロセス
リンク
リンクは、2つのプロセス間で作成できる特定の種類のリレーションシップです。このリレーションシップが設定されていて、一方のプロセスが予期しないスロー、エラー、または終了で停止した場合(エラーと例外を参照)、リンクされているもう一方のプロセスも停止します。
これは、エラーをできるだけ早く停止するという観点から有用な概念です。エラーが発生したプロセスがクラッシュしても、それに依存するプロセスがクラッシュしない場合、これらの依存プロセスはすべて、依存関係の消失に対処しなければなりません。それらを停止して、グループ全体を再起動する方が、通常は許容可能な代替手段です。リンクを使用すると、まさにこれを行うことができます。
2つのプロセス間にリンクを設定するために、Erlangにはプリミティブ関数link/1があります。これはPidを引数として受け取ります。呼び出されると、この関数は、現在のプロセスとPidで識別されるプロセス間にリンクを作成します。リンクを解除するには、unlink/1を使用します。リンクされたプロセスのいずれかがクラッシュすると、何が起こったかについての情報を含む特殊な種類のメッセージが送信されます。プロセスが自然な原因で停止する場合(つまり、関数の実行が完了した場合)、そのようなメッセージは送信されません。linkmon.erlの一部として、この新しい関数を最初に紹介します。
myproc() ->
timer:sleep(5000),
exit(reason).
次の呼び出しを試行し(各spawnコマンドの間に5秒間待つ)、リンクが2つのプロセス間に設定されている場合にのみ、シェルが「reason」のためにクラッシュすることを確認してください。
1> c(linkmon).
{ok,linkmon}
2> spawn(fun linkmon:myproc/0).
<0.52.0>
3> link(spawn(fun linkmon:myproc/0)).
true
** exception error: reason
あるいは、図で示すと
しかし、この{'EXIT', B, Reason}メッセージは、通常のtry ... catchではキャッチできません。これを行うには、他のメカニズムを使用する必要があります。後でそれらを見ていきます。
リンクは、すべて一緒に停止する必要があるより大きなプロセスグループを確立するために使用されることに注意することが重要です。
chain(0) ->
receive
_ -> ok
after 2000 ->
exit("chain dies here")
end;
chain(N) ->
Pid = spawn(fun() -> chain(N-1) end),
link(Pid),
receive
_ -> ok
end.
この関数は整数Nを受け取り、N個のプロセスを互いにリンクして開始します。N-1引数を次の「チェーン」プロセス(spawn/1を呼び出す)に渡すことができるように、呼び出しを匿名関数でラップしているので、引数は不要になります。spawn(?MODULE, chain, [N-1])を呼び出すと、同様の処理が行われます。
ここでは、多くのプロセスが互いにリンクされ、後続のプロセスが終了するたびに停止します。
4> c(linkmon).
{ok,linkmon}
5> link(spawn(linkmon, chain, [3])).
true
** exception error: "chain dies here"
そしてご覧のように、シェルは他のプロセスからの終了シグナルを受信します。生成されたプロセスと下向きのリンクの図解を示します。
[shell] == [3] == [2] == [1] == [0] [shell] == [3] == [2] == [1] == *dead* [shell] == [3] == [2] == *dead* [shell] == [3] == *dead* [shell] == *dead* *dead, error message shown* [shell] <-- restarted
linkmon:chain(0)を実行しているプロセスが終了すると、エラーはリンクのチェーンを伝播し、その結果、シェルプロセス自体も終了します。クラッシュはリンクされたプロセスのいずれかで発生する可能性があります。リンクは双方向であるため、他のプロセスがそれに追随するためには、1つが終了するだけで十分です。
注記:シェルから別のプロセスを終了する必要がある場合は、関数exit/2を使用できます。これは次のように呼び出されます:exit(Pid, Reason)。必要に応じて試してみてください。
注記:リンクはスタックできません。同じ2つのプロセスに対してlink/1を15回呼び出した場合、それらの間に存在するリンクは1つだけで、unlink/1を1回呼び出すだけで解除できます。
link(spawn(Function))またはlink(spawn(M,F,A))は複数のステップで行われることに注意することが重要です。場合によっては、リンクが設定される前にプロセスが終了し、予期しない動作を引き起こす可能性があります。このため、関数spawn_link/1-3が言語に追加されました。これはspawn/1-3と同じ引数を受け取り、プロセスを作成し、link/1があるかのようにリンクしますが、すべてアトミック操作として実行されます(操作は単一のものに結合され、失敗するか成功するかのどちらかですが、それ以外は何もありません)。これは一般的に安全であると考えられており、括弧のセットも節約できます。
罠だ!
さて、リンクとプロセスの終了に戻りましょう。プロセス間のエラー伝播は、メッセージパッシングと同様のプロセスを通じて行われますが、シグナルと呼ばれる特殊なタイプのメッセージを使用します。終了シグナルは、プロセスに自動的に作用し、動作中にプロセスを終了させる「秘密」メッセージです。
既に何度も述べてきたように、信頼性の高いアプリケーションには、プロセスを迅速に終了および再起動できる必要があります。現時点では、リンクは終了部分を実行するのに適しています。不足しているのは再起動です。
プロセスを再起動するには、まずそれが終了したことを知る方法が必要です。これは、リンクの上にレイヤー(ケーキの美味しいフロスティング)を追加し、システムプロセスと呼ばれる概念を使用することで実現できます。システムプロセスは基本的に通常のプロセスと同じですが、終了シグナルを通常のメッセージに変換できます。これは、実行中のプロセスでprocess_flag(trap_exit, true)を呼び出すことによって行われます。例ほど雄弁な言葉はありませんので、例で説明します。チェーンの例を、先頭にシステムプロセスを追加してやり直してみます。
1> process_flag(trap_exit, true).
true
2> spawn_link(fun() -> linkmon:chain(3) end).
<0.49.0>
3> receive X -> X end.
{'EXIT',<0.49.0>,"chain dies here"}
あ!今、興味深いことが起こります。図に戻ると、何が起こるかは次のようになります。
[shell] == [3] == [2] == [1] == [0]
[shell] == [3] == [2] == [1] == *dead*
[shell] == [3] == [2] == *dead*
[shell] == [3] == *dead*
[shell] <-- {'EXIT,Pid,"chain dies here"} -- *dead*
[shell] <-- still alive!
そして、これがプロセスの迅速な再起動を可能にするメカニズムです。システムプロセスを使用するプログラムを作成することで、何かが終了したかどうかをチェックし、失敗するたびに再起動するだけのプロセスを簡単に作成できます。これらのテクニックを実際に適用する次の章で、これについてさらに詳しく説明します。
例外の章で見た例外関数に戻り、終了をトラップするプロセス周辺での動作を示します。まず、システムプロセスなしで実験するための基礎を設定します。隣接するプロセスで、キャッチされないスロー、エラー、および終了の結果を順番に示します。
- 例外ソース:
spawn_link(fun() -> ok end) - トラップされていない結果:- なし -
- トラップされた結果:{'EXIT', <0.61.0>, normal}
- プロセスは問題なく正常に終了しました。これは
catch exit(normal)の結果と少し似ていますが、失敗したプロセスを知るためにPIDがタプルに追加されていることに注意してください。 - 例外ソース:
spawn_link(fun() -> exit(reason) end) - トラップされていない結果:** exception exit: reason
- トラップされた結果:{'EXIT', <0.55.0>, reason}
- プロセスはカスタム理由で終了しました。この場合、終了がトラップされていないと、プロセスはクラッシュします。そうでなければ、上記のメッセージが表示されます。
- 例外ソース:
spawn_link(fun() -> exit(normal) end) - トラップされていない結果:- なし -
- トラップされた結果:{'EXIT', <0.58.0>, normal}
- これは、正常に終了するプロセスをシミュレートします。場合によっては、例外的なことが何も起こっていない状態で、プログラムの通常のフローの一部としてプロセスを終了したい場合があります。これがその方法です。
- 例外ソース:
spawn_link(fun() -> 1/0 end) - トラップされていない結果:Error in process <0.44.0> with exit value: {badarith, [{erlang, '/', [1,0]}]}
- トラップされた結果:{'EXIT', <0.52.0>, {badarith, [{erlang, '/', [1,0]}]}}
- エラー(
{badarith, Reason})は、try ... catchブロックでは決してキャッチされず、'EXIT'にバブルアップします。この時点で、exit(reason)と同じように動作しますが、何が起こったかについての詳細を示すスタックトレースが追加されます。 - 例外ソース:
spawn_link(fun() -> erlang:error(reason) end) - トラップされていない結果:Error in process <0.47.0> with exit value: {reason, [{erlang, apply, 2}]}
- トラップされた結果:{'EXIT', <0.74.0>, {reason, [{erlang, apply, 2}]}}
1/0とほぼ同じです。これは正常です。erlang:error/1はまさにそれを行うことを目的としています。- 例外ソース:
spawn_link(fun() -> throw(rocks) end) - トラップされていない結果:Error in process <0.51.0> with exit value: {{nocatch, rocks}, [{erlang, apply, 2}]}
- トラップされた結果:{'EXIT', <0.79.0>, {{nocatch, rocks}, [{erlang, apply, 2}]}}
throwはtry ... catchでは決してキャッチされないため、エラーにバブルアップし、それがEXITにバブルアップします。終了をトラップしないと、プロセスは失敗します。そうでなければ、うまく処理します。
そして、通常の例外についてはこれくらいです。状況は正常です。すべてうまくいきます。例外的なことが起こります。プロセスが終了し、さまざまなシグナルが送られます。
次にexit/2があります。これは、Erlangプロセスの銃に相当します。安全に離れた場所から別のプロセスを終了できます。可能な呼び出しをいくつか示します。
- 例外ソース:
exit(self(), normal) - トラップされていない結果:** exception exit: normal
- トラップされた結果:{'EXIT', <0.31.0>, normal}
- 終了をトラップしない場合、
exit(self(), normal)はexit(normal)と同じように動作します。そうでなければ、外部プロセスの終了からのリンクをリッスンした場合と同じ形式のメッセージを受信します。 - 例外ソース:
exit(spawn_link(fun() -> timer:sleep(50000) end), normal) - トラップされていない結果:- なし -
- トラップされた結果:- なし -
- これは基本的に
exit(Pid, normal)への呼び出しです。このコマンドは役に立ちません。プロセスは、引数としてnormalという理由でリモートから終了できないためです。 - 例外ソース:
exit(spawn_link(fun() -> timer:sleep(50000) end), reason) - トラップされていない結果:** exception exit: reason
- トラップされた結果:{'EXIT', <0.52.0>, reason}
- これは、reason自体のために外部プロセスが終了したものです。外部プロセスが自分自身で
exit(reason)を呼び出した場合と同じように見えます。 - 例外ソース:
exit(spawn_link(fun() -> timer:sleep(50000) end), kill) - トラップされていない結果:** exception exit: killed
- トラップされた結果:{'EXIT', <0.58.0>, killed}
- 驚くべきことに、メッセージは終了プロセスからスパウナーに変更されます。スパウナーは
killではなくkilledを受信します。これは、killが特別な終了シグナルであるためです。これについては後で詳しく説明します。 - 例外ソース:
exit(self(), kill) - トラップされていない結果:** exception exit: killed
- トラップされた結果:** exception exit: killed
- おっと、見てください。これは実際にはトラップできないようです。何かを確認しましょう。
- 例外発生元:
spawn_link(fun() -> exit(kill) end) - トラップされていない結果:** exception exit: killed
- 捕捉された結果: {'EXIT', <0.67.0>, kill}
- さて、これはややこしくなってきました。別のプロセスが
exit(kill)で自身を終了し、終了を捕捉していない場合、私たちのプロセスもkilledという理由で終了します。しかし、終了を捕捉する場合、そうはなりません。
ほとんどの終了理由は捕捉できますが、プロセスを強制的に終了したい状況もあります。例えば、終了を捕捉しているプロセスが無限ループに陥り、メッセージを全く読み取らない場合などです。kill理由は、捕捉できない特別なシグナルとして機能します。これにより、killで終了させたプロセスは確実に死滅します。通常、killは、他のすべての手法が失敗した場合の最後の手段です。
kill理由は捕捉できないため、他のプロセスがメッセージを受信した場合はkilledに変更する必要があります。変更しなければ、それにリンクされた他のプロセスはすべて同じkill理由で終了し、さらにその隣接プロセスを終了させるという連鎖反応が起こります。
これはまた、別のリンクされたプロセスから受信した場合exit(kill)がkilledのように見える理由(シグナルが変更され、連鎖反応を防ぐため)と、ローカルで捕捉した場合killのように見える理由を説明しています。
これらすべてが混乱するようでしたら、心配しないでください。多くのプログラマが同じように感じています。終了シグナルはやや奇妙なものです。幸い、上記で説明したもの以上に特別なケースはほとんどありません。これらを理解すれば、Erlangの並行エラー管理の大部分を問題なく理解できます。
モニター
さて、プロセスを強制終了することが望ましいとは限りません。自分が消滅したときに世界を巻き添えにしたくないかもしれません。むしろ、監視者になりたいかもしれません。その場合は、モニターが役立ちます。
より具体的に言うと、モニターはリンクの特殊なタイプで、2つの違いがあります。
- 一方向性です。
- スタックできます。
モニターは、あるプロセスが別のプロセスの状況を知りたいが、どちらのプロセスも互いに不可欠ではない場合に役立ちます。
上記のように、参照をスタックできることも利点です。一見無意味に見えるかもしれませんが、他のプロセスの状況を知る必要があるライブラリの記述には非常に役立ちます。
リンクは、主に組織的な構成要素です。アプリケーションのアーキテクチャを設計する際に、どのプロセスがどのジョブを実行し、何が何に依存するのかを決定します。一部のプロセスは他のプロセスを監視し、一部のプロセスは双子のプロセスなしでは生き残れません。この構造は通常、事前にわかっている固定されたものです。リンクはこの用途に適しており、それ以外の用途に使用すべきではありません。
しかし、呼び出す2つか3つの異なるライブラリがあり、それらのすべてがプロセスの生存状況を知る必要がある場合はどうでしょうか?これに対してリンクを使用すると、プロセスをアンリンクする必要があるたびにすぐに問題が発生します。リンクはスタックできないため、1つをアンリンクするとすべてをアンリンクし、他のライブラリによって設定されたすべての仮定を台無しにします。これは非常に問題です。そのため、スタック可能なリンクが必要であり、モニターが解決策となります。モニターは個別に削除できます。さらに、一方向性であることは、他のプロセスがライブラリを認識する必要がないため、ライブラリでは便利です。
モニターはどのように見えるでしょうか?簡単です。設定してみましょう。関数はerlang:monitor/2で、最初の引数はアトムprocess、2番目の引数はpidです。
1> erlang:monitor(process, spawn(fun() -> timer:sleep(500) end)).
#Ref<0.0.0.77>
2> flush().
Shell got {'DOWN',#Ref<0.0.0.77>,process,<0.63.0>,normal}
ok
監視しているプロセスが停止するたびに、このようなメッセージを受信します。メッセージは{'DOWN', MonitorReference, process, Pid, Reason}です。参照は、プロセスをデモニターするために使用されます。モニターはスタック可能なので、複数をデモニターできることに注意してください。参照を使用することで、それぞれをユニークに追跡できます。また、リンクと同様に、プロセスを生成しながら監視する原子関数spawn_monitor/1-3もあります。
3> {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end).
{<0.73.0>,#Ref<0.0.0.100>}
4> erlang:demonitor(Ref).
true
5> Pid ! die.
die
6> flush().
ok
この場合、クラッシュする前に他のプロセスをデモニターしていたため、その死に関する痕跡がありませんでした。関数demonitor/2も存在し、もう少し多くの情報を提供します。2番目のパラメータはオプションのリストにすることができます。存在するものはinfoとflushの2つだけです。
7> f().
ok
8> {Pid, Ref} = spawn_monitor(fun() -> receive _ -> exit(boom) end end).
{<0.35.0>,#Ref<0.0.0.35>}
9> Pid ! die.
die
10> erlang:demonitor(Ref, [flush, info]).
false
11> flush().
ok
infoオプションは、削除しようとしたときにモニターが存在したかどうかを知らせます。そのため、式10はfalseを返しました。flushをオプションとして使用すると、存在する場合、メールボックスからDOWNメッセージが削除され、flush()は現在のプロセスのメールボックスに何も見つかりません。
プロセスの名前付け
リンクとモニターを理解したところで、まだ解決されていない問題があります。linkmon.erlモジュールの次の関数を使用します。
start_critic() ->
spawn(?MODULE, critic, []).
judge(Pid, Band, Album) ->
Pid ! {self(), {Band, Album}},
receive
{Pid, Criticism} -> Criticism
after 2000 ->
timeout
end.
critic() ->
receive
{From, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {self(), "They are great!"};
{From, {"System of a Downtime", "Memoize"}} ->
From ! {self(), "They're not Johnny Crash but they're good."};
{From, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {self(), "Simply incredible."};
{From, {_Band, _Album}} ->
From ! {self(), "They are terrible!"}
end,
critic().
ここでは、お店を巡って音楽を買いに行くふりをしましょう。興味深いアルバムはいくつかありますが、なかなか決められません。批評家の友人に電話することにします。
1> c(linkmon).
{ok,linkmon}
2> Critic = linkmon:start_critic().
<0.47.0>
3> linkmon:judge(Critic, "Genesis", "The Lambda Lies Down on Broadway").
"They are terrible!"
太陽嵐(現実的なものを探しています)のため、接続が切断されました。
4> exit(Critic, solar_storm). true 5> linkmon:judge(Critic, "Genesis", "A trick of the Tail Recursion"). timeout
困りました。もうアルバムの批評を得ることができません。批評家を存続させるために、停止したときに再起動するだけの基本的な「スーパーバイザ」プロセスを作成します。
start_critic2() ->
spawn(?MODULE, restarter, []).
restarter() ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, critic, []),
receive
{'EXIT', Pid, normal} -> % not a crash
ok;
{'EXIT', Pid, shutdown} -> % manual termination, not a crash
ok;
{'EXIT', Pid, _} ->
restarter()
end.
ここで、リスターターは独自のプロセスになります。リスターターは批評家のプロセスを開始し、異常な原因で終了した場合は、restarter/0がループして新しい批評家を作成します。必要に応じて批評家を強制終了する方法として、{'EXIT', Pid, shutdown}に関する節を追加しました。
私たちのアプローチの問題は、批評家のPidを見つける方法がなく、そのため意見を求めることができないことです。Erlangがこれを解決するために持つソリューションの1つは、プロセスに名前を付けることです。
プロセスに名前を付けることで、予測不可能なpidをアトムに置き換えることができます。このアトムは、メッセージを送信する場合にPidとまったく同じように使用できます。プロセスに名前を付けるには、関数erlang:register/2を使用します。プロセスが終了すると、自動的に名前が失われます。または、unregister/1を使用して手動で行うこともできます。registered/0を使用して、登録されているすべてのプロセスのリストを取得するか、シェルコマンドregs()を使用してより詳細なリストを取得できます。ここで、restarter/0関数を次のように書き換えることができます。
restarter() ->
process_flag(trap_exit, true),
Pid = spawn_link(?MODULE, critic, []),
register(critic, Pid),
receive
{'EXIT', Pid, normal} -> % not a crash
ok;
{'EXIT', Pid, shutdown} -> % manual termination, not a crash
ok;
{'EXIT', Pid, _} ->
restarter()
end.
ご覧のとおり、register/2はPidに関係なく、常に批評家に「critic」という名前を付けます。次に、抽象関数からPidを渡す必要性を削除する必要があります。これを試してみましょう。
judge2(Band, Album) ->
critic ! {self(), {Band, Album}},
Pid = whereis(critic),
receive
{Pid, Criticism} -> Criticism
after 2000 ->
timeout
end.
ここでは、Pid = whereis(critic)という行を使用して、receive式でパターンマッチを行うために批評家のプロセス識別子を見つけます。このpidとマッチングさせたいのは、正しいメッセージと確実にマッチングさせるためです(話している間にメールボックスに500個のメッセージがある可能性があります!)。ただし、これが問題の原因になる可能性があります。上記のコードは、関数の最初の2行間で批評家のpidが同じであると仮定しています。しかし、以下のようなことが起こることは完全に考えられます。
1. critic ! Message
2. critic receives
3. critic replies
4. critic dies
5. whereis fails
6. critic is restarted
7. code crashes
または、これも可能性があります。
1. critic ! Message
2. critic receives
3. critic replies
4. critic dies
5. critic is restarted
6. whereis picks up
wrong pid
7. message never matches
正しく行わない場合、異なるプロセスで問題が発生する可能性があると、別のプロセスで問題が発生する可能性があります。この場合、criticアトムの値は複数のプロセスから見ることができます。これは共有状態として知られています。criticの値は、事実上同時に複数のプロセスによってアクセスおよび変更できるため、情報の一貫性がなくなり、ソフトウェアエラーが発生します。このようなものの一般的な用語は競合状態です。競合状態は、イベントのタイミングに依存するため、特に危険です。ほぼすべての並列言語では、このタイミングは、プロセッサの稼働状況、プロセスの移動先、プログラムによって処理されているデータなど、予測できない要因に依存します。
クーレイドを飲みすぎないでください。
Erlangは通常、競合状態やデッドロックがなく、並列コードを安全にするという話を聞いたことがあるかもしれません。これは多くの状況で真実ですが、コードが本当に安全であると決して仮定しないでください。名前付きプロセスは、並列コードが間違ってしまう可能性のある複数の方法の1つの例に過ぎません。
その他の例としては、コンピュータ上のファイルへのアクセス(変更するため)、多くの異なるプロセスからの同じデータベースレコードの更新などがあります。
幸いなことに、名前付きプロセスが同じであると仮定しなければ、上記のコードを比較的簡単に修正できます。代わりに、参照(make_ref()で作成)を、メッセージを一意に識別するユニークな値として使用します。critic/0関数をcritic2/0に、judge/3をjudge2/2に書き直す必要があります。
judge2(Band, Album) ->
Ref = make_ref(),
critic ! {self(), Ref, {Band, Album}},
receive
{Ref, Criticism} -> Criticism
after 2000 ->
timeout
end.
critic2() ->
receive
{From, Ref, {"Rage Against the Turing Machine", "Unit Testify"}} ->
From ! {Ref, "They are great!"};
{From, Ref, {"System of a Downtime", "Memoize"}} ->
From ! {Ref, "They're not Johnny Crash but they're good."};
{From, Ref, {"Johnny Crash", "The Token Ring of Fire"}} ->
From ! {Ref, "Simply incredible."};
{From, Ref, {_Band, _Album}} ->
From ! {Ref, "They are terrible!"}
end,
critic2().
そして、critic/0ではなくcritic2/0を生成するようにrestarter/0を変更します。これで、他の関数は正常に動作するはずです。ユーザーは違いに気づきません。関数の名前を変更し、パラメータの数を変更したため、違いに気付くでしょうが、実装の詳細が変更された理由やその重要性はわかりません。ユーザーに見えるのは、コードが単純になり、関数呼び出しでpidを送信する必要がなくなったことです。
6> c(linkmon).
{ok,linkmon}
7> linkmon:start_critic2().
<0.55.0>
8> linkmon:judge2("The Doors", "Light my Firewall").
"They are terrible!"
9> exit(whereis(critic), kill).
true
10> linkmon:judge2("Rage Against the Turing Machine", "Unit Testify").
"They are great!"
そして今、批評家を終了しても、新しい批評家がすぐに現れて問題を解決します。これが名前付きプロセスの有用性です。登録されたプロセスなしでlinkmon:judge/2を呼び出そうとした場合、関数内の!演算子によってbad argumentエラーがスローされ、名前付きプロセスに依存するプロセスはそれらなしでは実行できないことが保証されます。
注記: 前のテキストを覚えているなら、アトムは限られた(ただし多い)数で使用できます。動的なアトムを作成することは決してありません。つまり、プロセスの名前付けは、VMのインスタンスに固有の重要なサービスと、アプリケーションの実行時間全体に存在する必要があるプロセスに限定する必要があります。
名前付きプロセスが必要だが、一時的なものか、VMに固有のものがない場合、代わりにグループとして表す必要があることを意味します。クラッシュした場合、動的な名前を使用しようとするのではなく、それらをまとめてリンクして再起動することが賢明な選択肢かもしれません。
次の章では、Erlangを使用した最近の並行プログラミングの知識を、実際のアプリケーションを作成することで実践します。