Erlang の gen_server ビヘイビアを学ぶ
Erlang/OTP の gen_server ビヘイビアについて学びたいと思います。
クライアント・サーバの原則
クライアントサーバーモデルは、複数の異なるクライアントで共有するリソース管理に使用されます。サーバーは、このリソースの管理を担当します。
図の実線はQuery, 破線はReplyを表す。
サンプルコード
gen_server のサンプルコードとして sample_queue.erl というファイル名で下記のコードを作成します。 このコードは、gen_server のビヘイビアを使って、キューの機能を提供します。sample_queue.erl モジュールは、gen_server のコールバックモジュールです。
in/1
関数で gen_server にアイテムを登録して、out/0
関数で gen_server からアイテムを取得します。
ここで、start_link/0
, in/1
, out/0
関数はインターフェース関数と呼ばれます。init/1
, handle_call/3
, handle_cast/2
関数はコールバック関数と呼ばれます。インターフェース関数とコールバック関数は同じモジュールに配置されます。一般的に、1つのプロセスに対応するコードは1つのモジュールにまとめられます。
ただし、インターフェース関数の実行は gen_server を起動したり、gen_server にリクエストを送信する Erlang プロセス側で実行されます。ここでのコールバック関数は、gen_server として起動する Erlang プロセス側で実行されるものであることに注意してください。
-module(sample_queue). -behaviour(gen_server). -export([start_link/0, in/1, out/0]). -export([init/1, handle_call/3, handle_cast/2]). start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). in(Item) -> gen_server:cast(?MODULE, {in, Item}). out() -> gen_server:call(?MODULE, out). init(_Args) -> {ok, queue:new()}. handle_call(out, _From, Queue) -> case queue:out(Queue) of {{value, Item}, NewQueue} -> {reply, Item, NewQueue}; {empty, NewQueue} -> {reply, empty, NewQueue} end. handle_cast({in, Item}, Queue) -> NewQueue = queue:in(Item, Queue), {noreply, NewQueue}.
下記は使用例です。下記では、sample_queue モジュールの gen_server を起動し、その後、sample_queue モジュールのAPIを使って、a, b, c というアイテムを登録し、その後は逆にキューのアイテムを空になるまで取り出しています。
> c(sample_queue). {ok,sample_queue} > sample_queue:start_link(). {ok,<0.68.0>} > sample_queue:in(a). ok > sample_queue:in(b). ok > sample_queue:in(c). ok > sample_queue:out(). a > sample_queue:out(). b > sample_queue:out(). c > sample_queue:out(). empty
gen_server の起動
サンプルコードの start_link/0
を呼び出すことで gen_server が起動します。
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
start_link/0
は gen_server:start_link/4
関数を呼び出します。この関数は gen_server として新しいプロセスを spawn し、そのプロセスに link します。
最初の引数 {local, ?MODULE}
は gen_server プロセスの名前を指定します。この gen_server はローカルのErlangノードに ?MODULE
として登録されます。?MODULE
はモジュール名を表します。つまり、この例では sample_queue
です。
名前を省略すると、gen_server の名前は登録されません。代わりに、その pid を使用する必要があります。名前は{global, Name}
としても指定できます。この場合、gen_server は global:register_name/2
を使用して登録されます。
第2の引数も ?MOUDLE
です。これは gen_server のコールバックモジュールの名前を指定します。たいてい start_link
関数をコールバックモジュールに記述するので、そのモジュール自身の名前が使われます。
第3の引数 []
は、コールバック関数 init/1
関数にそのまま渡される1つの erlang term です。今回のサンプルコードでは、データを必要としないので単に無視しています。
第4の引数は[]
は、オプションのリストです。ここでは、gen_server の概要を掴みたいので、オプションには触れません。
gen_server プロセスの名前の登録が成功すると、新しい gen_server プロセスはコールバック関数 sample_queue:init([])
を呼び出します。 init は {ok, State}
を返すことが期待されます。State
は gen_server の内部状態です。 今回の場合は空の queue です。
init(_Args) -> {ok, queue:new()}.
gen_server:start_link
は同期的に実行されます。 gen_server が init 関数によって初期化され、リクエストを受信する準備ができるまで戻りません。
gen_server が監視ツリーの一部である場合、つまりスーパーバイザーによって開始された場合は、gen_server:start_link
を使用しなければなりません。 これとは別に、スタンドアロンの gen_server を起動する gen_server:start
があります。この関数で起動した gen_server は、監視ツリーには含まれません。
同期リクエスト - call
同期リクエストの out/0
は、 gen_server:call/2
を使用して実装されています。同期リクエストではリクエストに対して gen_server からレスポンスが返ってきます。
out() -> gen_server:call(?MODULE, out).
?MODULE
はモジュール名であり、これが gen_server の名前になっています。サンプルコードでは sample_queue
という atom です。これは gen_server の起動に使用される名前と一致する必要があります。 out
が gen_server への実際のリクエストです。
リクエストはメッセージになり、?MODULE
(sample_queue
) という名前の gen_server に送信されます。 リクエストを受信すると、gen_server は handle_call(Request, From, State)
を呼び出します。これは、タプル{reply, Reply, NewState}
を返すことが期待されています。 Reply
はクライアントに送り返されるレスポンスであり、NewState
は gen_server の内部状態の新しい値です。
handle_call(out, _From, Queue) -> case queue:out(Queue) of {{value, Item}, NewQueue} -> {reply, Item, NewQueue}; {empty, NewQueue} -> {reply, empty, NewQueue} end.
この場合、out
リクエストを受け取る handle_call/3
関数の応答は、Queue
に Item
があればそのItem
を返し、なければ empty
を返します。新しい状態はQueue
からItem
を削除したキュー NewQueue
です。
したがって、queue_sample:out/1
の呼び出しは、Item
または empty
を返し、gen_server は新しいリクエストを待機します。待機中の gen_server の内部状態はリクエストに含まれていたアイテムが削除されたキューで更新されています。
非同期リクエスト - cast
非同期リクエストの in/1
は、 gen_server:cast/2
を使用して実装されています。非同期リクエストではリクエストに対して gen_server からレスポンスは返って来ません。単に ok
が返ってきます。
in(Item) -> gen_server:cast(?MODULE, {in, Item}).
?MODULE
はモジュール名であり、これが gen_server の名前になっています。サンプルコードでは sample_queue
という atom です。{in, Item}
が gen_server への実際のリクエストです。
リクエストはメッセージになり、gen_server のプロセス送信すると、ok
が返ってきます。この ok
は gen_server のプロセスのレスポンスではなく、単に gen_server:cast
関数が ok
を返しているだけで、リクエストの結果ではないことに注意してください。また、メッセージを送信しただけで、gen_server がメッセージを受け取ったかどうかも保証されません。
gen_server のプロエスはリクエストを受信すると、handle_cast(Request, State)
を呼び出します。これは、タプル{noreply, NewState}
を返すことが期待されています。 NewState
は、gen_serverの内部状態の新しい値です。
handle_cast({in, Item}, Queue) -> NewQueue = queue:in(Item, Queue), {noreply, NewQueue}.
この場合、新しい状態は Queue
にリクエストに含まれる Item
を登録した新しいキュー NewQueue
です。これで、 gen_server プロセスは内部状態のキューに Item
を登録しました。
停止
監視ツリーでの停止
gen_server が監視ツリーの一部である場合、停止機能は必要ありません。gen_serverは、スーパーバイザーによって自動的に終了します。これがどのように行われるかは、スーパーバイザーに設定されたシャットダウン戦略によって定義されます。
終了前にクリーンアップする必要がある場合は、シャットダウン戦略にタイムアウト値にし、関数initで終了シグナルを補足できるように gen_server を設定する必要があります。 シャットダウンするように命令されると、gen_server はコールバック関数 terminate(shutdown, State)
を呼び出します。この teminate/2
関数に後始末をする処理を記述します。
init(Args) -> ..., process_flag(trap_exit, true), ..., {ok, State}. ... terminate(shutdown, State) -> ..code for cleaning up here.. ok.
スタンドアローンでの停止
もし、gen_server が監視ツリーの一部でなかった場合には、 stop 関数が役に立ちます。
... export([stop/0]). ... stop() -> gen_server:cast(?MODULE, stop). ... handle_cast(stop, State) -> ... create new state {stop, normal, NewState}; handle_cast({in, Item}, Queue) -> .... ... terminate(normal, NewState) -> ..code for cleaning up here.. ok.
停止リクエストを処理するコールバック関数は、タプル {stop, normal, NewState}
を返します。ここで、normal は正常終了であり、NewState
はgen_server の状態の新しい値です。 これにより、gen_serverは terminate(normal, NewState)
を呼び出してから、正常に終了します。
他のメッセージの扱い
gen_server がリクエスト以外のメッセージを受信できるようにする場合、コールバック関数 handle_info(Info, State)
を実装してそれらを処理することができます。 他のメッセージの例は、gen_server が他のプロセス(スーパーバイザーではない)にリンクされていて、終了シグナルをトラップしている場合、そのプロセスの終了メッセージです。
handle_info({'EXIT', Pid, Reason}, State) -> ..code to handle other process exits here.. {noreply, NewState}.
以上で、おおよその gen_server の実装イメージを把握しました。