Erlang 基礎ポイント5 - 並列処理2

今日は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を使うサンプルです。

% spawn_sub.erlファイルです。
% spawn_test.erlとspawn_link_test.erlで共通で使うための関数を持っています。
-module(spawn_sub).
-export([loop/1]).

% 2秒おきにメッセージ出力
loop(Msg) ->
  receive
    % 終了処理
    exit ->
      exit;
    % わざと、0で割ってエラーを起こす。
    % コンパイル時、警告が出ますが、学習用なので、無視して下さい。
    error ->
      1 / 0
  after 2000 ->
    io:format("Process ~p ~n", [Msg]),
    loop(Msg)
  end.

% spawn_test.erlファイルです。
-module(spawn_test).
-import(spawn_sub, [loop/1]).
-export([spawn_test/0]).

% テストメソッド
spawn_test() ->
  spawn(fun p/0).

% 親Erlangプロセス
p() ->
  CPid = spawn(fun c/0),
  % 後でchildと言う名前で子Erlangプロセスにerrorメッセージを送れるようになる。
  register(child, CPid),
  loop("Parent").

% 子Erlangプロセス
c() ->
  loop("Child").

% REPL環境で試したものです。
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.% 子Erlangプロセスにエラーを起こします。

=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" % Childが出力されなくなりました。しかし、Parentは出力し続けています。
Process "Parent" 
Process "Parent" 

次はspawn_linkを使うサンプルです。

% spawn_link_test.erlファイルです。
-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),  % ここだけがspawn_testと違います。
  register(child, CPid),
  loop("Parent").

c() ->
  loop("Child").

% REPL環境で試したものです。
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. % 子Erlangプロセスにエラーを起こします。

=ERROR REPORT==== 28-May-2014::13:41:59 === % 親Erlangプロセスも子Erlangプロセスも終了してしまいました。
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のエラーを捕らえて何かをすることができます。
以下のサンプルを見てみましょう。

% system_process_test.erlファイルです。
-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.

% REPL環境で試したものです。
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. % 子Erlangプロセスにエラーを起こします。

=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,  % PでCのエラーをちゃんと捕らえています。
                                       [{system_process_test,loop,1}]})
error
Process "Parent"  % エラーを捕らえた後もPは動きつづけています。
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関数を使えばいいです。
以下のサンプルを参考して下さい。

% link_test.erlファイルです。
-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").

% REPL環境で試したものです。
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. % 子Erlangプロセスにエラーを起こします。

=ERROR REPORT==== 28-May-2014::14:01:20 === % 親Erlangプロセスも子Erlangプロセスも止まりました。
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は並行処理プログラムの作成がとても簡単ですね。
次は並行処理の続きでモニタについて勉強したいと思います。

参考書

プログラミングErlang

プログラミングErlang