Erlangにはシステムメッセージというものが定義されていて、これを使って走行中のプロセスを中断して介入することができます。とは言っても、システムメッセージは何ら特殊なものではなくて、普通のErlangメッセージに過ぎません。{system, From, Request}という形のメッセージが来たらどう行動すべきかの約束があるだけなのです。
したがって、システムメッセージの規約を守らないプロセスの中断・再開はできません。この点で、OSカーネルがサポートする割り込みとは違います。でも、規約による割り込みのメカニズムが面白いので、単純化した例で紹介します。
実例:カウンター
まずは、実例のコード。
-module(counter).
-export([start/1, loop/1]).start(N) when is_integer(N) ->
spawn(?MODULE, loop, [N]).loop(N) ->
receive
up -> loop(N+1);
down -> loop(N-1);
stop -> exit(normal)
end.
これだけです。start/1に渡す引数がカウンターの初期値です。up, downというメッセージをプロセスに送ると、内部のカウンター値が変化します。外からカウンター値が見えないので困りますが、この点は今回の話ではどうでもいいことです(問題にしない)。
割り込みメッセージとその対処
さて、カウンターのコードを、「{break, Request}という形のメッセージが来たらそれに反応する」ように書き換えます。その反応とは、
- 現在のカウンター値NとRequestを引数にして、関数breakを呼ぶ
というものです。
ただし、(ここがミソなんですが)breakから戻ることを期待しません。breakから戻らなかったら、プロセスのメイン処理であるloopが途切れてしまいそうですが、continueという関数を前もって定義しておいて、breakからcontinueを呼んでもらうのです。
場合によっては、breakがプロセスを終了させたいと思うかもしれません。そのときは、割り込んだbreakが勝手にプロセスを終了させるのはまずいので、これまた事前に定義されたterminateを呼ぶというお約束にします。
割り込み可能なカウンター
-module(counter2).
-export([start/1, loop/1]).start(N) when is_integer(N) ->
spawn(?MODULE, loop, [N]).loop(N) ->
receive
up -> loop(N+1);
down -> loop(N-1);
stop -> exit(normal);
{break, Request} -> break(N, Request)
end.continue(N) ->
loop(N).terminate() ->
io:fwrite("~w is killed.~n", [self()]),
exit(break_killed).break(N, dump) ->
io:fwrite("N=~w~n", [N]),
continue(N);
break(_N, kill) ->
terminate();
break(N, _) ->
% ignore
continue(N).
continueとterminateは、先の前提とお約束に従って書かれたコールバックです。割り込んで処理する関数breakは好きに書けますが、ここではダンプと強制終了を実装しました。
> P = counter2:start(10).<0.84.0>
> P!{break, dump}.
N=10
{break,dump}
> P!up.
up
> P!{break, dump}.
N=11
{break,dump}
> P!{break, kill}.<0.84.0> is killed.
{break,kill}
>
背景
Erlangでは、末尾再帰は最適化されます。そうでないと、無限ループが書けません。プロセスのメイン処理はたいてい無限ループなので、末尾再帰最適化は必須です。しかし、別に再帰じゃなくても末尾呼び出し(last call)は常に最適化されます。ここで“最適化”とは、呼び側関数のスタックフレームを上書き使用して、スタック消費を節約することです。
末尾呼び出しをいくらしてもスタックが消費されないので、安心して、2つの(あるいはそれ以上の)関数のあいだで制御をピンポン(あるいはバスケットやサッカーのパス)のようにやり取りできます。
ただし、スタックフレームが上書きされるってことは、関数の実行状況が失われるので、後で制御が戻ってきたときに欲しいデータも呼び出し時に相手にあずけておきます(後で返してもらう)。breakに渡したカウンター値Nがそのようなデータです。このNは実際continueに渡されて戻ってきます。
今回の例では、breakに一時あずけて返してもらう値Nと、最後に呼び出して欲しい関数名continueを決め打ちしましたが、break({Module, Fun, Args}, Request) のように、自由に指定することもできます。
このような制御のやり取りの類<たぐい>は、コルーチンとかトランポリンと呼ばれることもあります。戻ってこない関数が、あたかも戻って続きが実行されるように見えるのは、継続という概念で説明できます。単純で実際的な割り込みのテクニックにも、こんな背景があるんですね :-)