今日はErlangの並行処理における例外処理について勉強したいと思います。
並行処理での例外処理
Erlang 基礎ポイント4で並行処理プログラムの作成が可能になりました。
しかし、現在実行中のErlangプロセス(=Pといいます)から子Erlangプロセス(=Cといいます)を起動し、
そのCからエラーが発生した時の対処方法はまだ勉強していません。
これができないと、Erlangプロセス全体が一つの塊として、機能するのは難しいですね。
まるで細胞同士が協力し、一つの大きい生き物が成り立つのに似ていると思います。
リンク
PからCを生成し、PとCは何の関係もないならば、それまです。
こういう場合、PやCに何かエラーが起こってもお互いは何の関係も影響もありません。
しかし、Cにエラーが起きて、Pも一緒にエラーを起こすか、エラーを修復する処理を
したい場合、PとCの間にリンクを作らなければなりません。
リンクは言葉通りにPとCが繋がっているのを意味し、
一方のErlangプロセスに何か起きた場合、
他方のプロセスにそれが伝わり、他方が自分も終了させたり他の処理をしたい時、使われます。
普通のErlangプロセスを生成する時は、spawn()関数を使いますが、
リンク付きのErlangプロセスを生成したい場合、spawn_link()関数を使います。
以下のサンプルでspawn()とspawn_link()を比較してみましょう。
生成した子Erlangプロセスでエラーが起きた時の動作の違いが分かります。
まず、spawnを使うサンプルです。
-module(spawn_sub).
-export([loop/1]).
loop(Msg) ->
receive
exit ->
exit;
error ->
1 / 0
after 2000 ->
io:format("Process ~p ~n", [Msg]),
loop(Msg)
end.
-module(spawn_test).
-import(spawn_sub, [loop/1]).
-export([spawn_test/0]).
spawn_test() ->
spawn(fun p/0).
p() ->
CPid = spawn(fun c/0),
register(child, CPid),
loop("Parent").
c() ->
loop("Child").
1> c(spawn_sub).
./spawn_sub.erl:15: Warning: this expression will fail with a 'badarith' exception
{ok,spawn_sub}
2> c(spawn_test).
{ok,spawn_test}
3> spawn_test:spawn_test().
<0.44.0>
Process "Child"
Process "Parent"
Process "Parent"
Process "Child"
Process "Child"
Process "Parent"
4> child ! error.
=ERROR REPORT==== 24-Jun-2014::10:12:28 ===
Error in process <0.45.0> with exit value: {badarith,[{spawn_sub,loop,1}]}
error
Process "Parent"
Process "Parent"
Process "Parent"
次はspawn_linkを使うサンプルです。
-module(spawn_link_test).
-import(spawn_sub, [loop/1]).
-export([spawn_link_test/0]).
spawn_link_test() ->
spawn(fun p/0).
p() ->
CPid = spawn_link(fun c/0),
register(child, CPid),
loop("Parent").
c() ->
loop("Child").
1> c(spawn_sub).
./spawn_sub.erl:15: Warning: this expression will fail with a 'badarith' exception
{ok,spawn_sub}
2> c(spawn_link_test).
{ok,spawn_link_test}
3> spawn_link_test:spawn_link_test().
<0.44.0>
Process "Child"
Process "Parent"
Process "Parent"
Process "Child"
Process "Child"
Process "Parent"
Process "Parent"
Process "Child"
4> child ! error.
=ERROR REPORT==== 28-May-2014::13:41:59 ===
Error in process <0.45.0> with exit value: {badarith,[{spawn_sub,loop,1}]}
error
5>
spawnの方はCが死んでも、Pは動き続けていることが分かります。
つまり、CがどうなってもPは構いません。
なんというか、単細胞がただ分裂するような感じですね。
Javaでたとえると、単純にスレッドを起こしてそれでおしまいという感じです。
逆にspawn_linkの方はCが死んだら、Pも死ぬことが分かります。
プロセスが全部死ぬので、実際のシステムだったら、システムの停止を意味するので、まずいでしょうが、
spawnの代わりにspawn_linkを使うだけで、複数のErlangプロセスがひとかたまりとして、
動く(... 死ぬ)ようになる点はすごいと思います。
Javaでこれをやるためには親スレッドと子スレッドが通信するための何らかの
仕組み(フラグ変数、ブロッキングキュー、Latch等)が必要になってくるでしょう。
spawn_linkを使って、PとCの間にリンクが置けることが分かりました。
しかし、それだけでは、Cにエラーが起きて死んだ場合、Pも死ぬだけです。
Cが死んで、Pも死んで、PのPも死んで... 結果的にシステムが停止...
これでは実用性がないですし、耐久性があるシステムとはいえないですね。
システムのどこかにエラーが起きた場合、それを捕らえて、修復したり、一部の機能を停止したりして、
システムが動き続けるようにするのが理想的でしょう。
Cにエラーが起きて、Cが死んだ場合、Pも一緒に死ぬのではなく、
PがCのエラーを捕らえて何かをしたい場合、Pをシステムプロセスに指定する必要があります。
Pをシステムプロセスにすると、Cのエラーを捕らえて何かをすることができます。
以下のサンプルを見てみましょう。
-module(system_process_test).
-export([test/0]).
test() ->
spawn(fun p/0).
p() ->
process_flag(trap_exit, true),
CPid = spawn_link(fun c/0),
register(child, CPid),
loop("Parent").
c() ->
loop("Child").
loop(Msg) ->
receive
{'EXIT', Pid, Reason} ->
io:format("We catched error(Pid:~p, Reason:~p)~n", [Pid, Reason]),
loop(Msg);
exit ->
exit;
error ->
1 / 0
after 2000 ->
io:format("Process ~p ~n", [Msg]),
loop(Msg)
end.
2> c(system_process_test).
./system_process_test.erl:25: Warning: this expression will fail with a 'badarith' exception
{ok,system_process_test}
3> system_process_test:test().
<0.43.0>
Process "Child"
Process "Parent"
Process "Parent" 、
Process "Child"
Process "Child"
Process "Parent"
4> child ! error.
=ERROR REPORT==== 28-May-2014::13:55:31 ===
Error in process <0.44.0> with exit value: {badarith,[{system_process_test,loop,1}]}
We catched error(Pid:<0.44.0>, Reason:{badarith,
[{system_process_test,loop,1}]})
error
Process "Parent"
Process "Parent"
Process "Parent"
Process "Parent"
システムプロセスにしたい場合、process_flag(trap_exit, true)を呼び出せばいいです。
trueの代わりにfalseにすると、普通のErlangプロセスになりますが、
falseに指定することはあまりないと思います。
そもそもprocess_flag自体を呼び出さなければ、普通のErlangプロセスになるからです。
明示的なリンクの作成
spawn_linkでなく、spawnでErlangプロセスを作成しましたが、
後で親Erlangプロセスと子Erlangプロセスの間に
リンクを作成したい場合、link関数を使えばいいです。
以下のサンプルを参考して下さい。
-module(link_test).
-import(spawn_sub, [loop/1]).
-export([test/0]).
test() ->
spawn(fun p/0).
p() ->
CPid = spawn(fun c/0),
link(CPid),
register(child, CPid),
loop("Parent").
c() ->
loop("Child").
1> c(link_test).
{ok,link_test}
2> c(spawn_sub).
./spawn_sub.erl:15: Warning: this expression will fail with a 'badarith' exception
{ok,spawn_sub}
3> link_test:test().
<0.44.0>
Process "Child"
Process "Parent"
Process "Parent"
Process "Child"
Process "Child"
Process "Parent"
Process "Parent"
Process "Child"
4> child ! error.
=ERROR REPORT==== 28-May-2014::14:01:20 ===
Error in process <0.45.0> with exit value: {badarith,[{spawn_sub,loop,1}]}
error
5>
注意すべきは一見、spawn_linkはspawn + linkで代替できそうですが、実はそうではありません。
spawn_linkはErlangプロセスの生成とリンクの作成が原子的に実行されますが、
spawn + linkはErlangプロセスの生成(spawn)とリンクの作成(link)の間に隙があるので、
原子的に動くのではないからです。
spawnでErlangプロセスを生成した後、linkが実行される前のほんの少しの間に
生成したErlangプロセスが死ぬことがありえます。
今日はここまでにします。
ここまで勉強すると、ある程度、Erlangで並行処理プログラムを作れるのではないでしょうか。
Erlangは並行処理プログラムの作成がとても簡単ですね。
次は並行処理の続きでモニタについて勉強したいと思います。