gen_server
前に作った echo server を gen_server 化しようと思ったのですが、幾つか疑問点が出てきたので、まずは、自分が理解した事をまとめてみようと思います。
カウンタを保持するサーバの例
- 内部で一つの数値を保持している
- 初期値/最小値 0
- 最大値 100
開始 | gen_server_test.counter:start() |
終了 | gen_server_test.counter:stop() |
現在値を取得 | gen_server_test.counter:get() |
インクリメント | gen_server_test.counter:increment() |
デクリメント | gen_server_test.counter:decrement() |
-module(gen_server_test.counter). -behaviour(gen_server). -import(gen_server). % I/F -export([start/0, stop/0]). -export([get/0, increment/0, decrement/0]). % callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3 ]). % I/F start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). stop() -> gen_server:cast(?MODULE, stop). get() -> gen_server:call(?MODULE, get). increment() -> gen_server:call(?MODULE, increment). decrement() -> gen_server:call(?MODULE, decrement). % callbacks init(_) -> {ok, 0}. handle_cast(stop, State) -> {stop, normal, State}; handle_call(get, _, Count) -> {reply, Count, Count}; handle_call(increment, _, 100) -> {reply, 100, 100}; handle_call(increment, _, Count) -> {reply, Count+1, Count+1}; handle_call(decrement, _, 0) -> {reply, 0, 0}; handle_call(decrement, _, Count) -> {reply, Count-1, Count-1}. handle_info(_, State) -> {noreply, State}. terminate(normal, _) -> ok; terminate(_, _) -> ok. code_change(_, _, _) -> {ok, State}.
理解している箇所
開始
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
第一引数 {local, ?MODULE} は、新たに作ったプロセスを regster/2 を使って ?MODULE という名前で登録するという意味。
?MODULE じゃなくても、counter でも cooldaemon でも何でも良い。
{global, ?MODULE} と指定すると、global:register_name/2 を使うので、接続している全てのノードに ?MODULE と言う名前が通知される。
http://www.erlang.org/doc/man/global.html#register_name/2
また、{local, ?MODULE} と {global, ?MODULE} では、gen_server:call/2 や gen_server:call/2 の第一引数の指定が多少異なる(後述)。
第二引数 ?MODULE は、コールバック関数が存在するモジュール名。
第三引数 [] は、コールバック関数 init に渡す引数を設定できる。
例えば…
start(N) -> gen_server:start_link({local, ?MODULE}, ?MODULE, [N], []). init([N]) -> {ok, N}.
こんな感じで、引数を渡せる。
第四引数 [] は、gen_server のオプションを指定できる。
詳しく調べていないので、また今度。
init(_) -> {ok, 0}.
プロセスが正しく起動すると、init/1 関数が呼ばれる。
init/1 関数は、{ok, State} を返すと成功とみなされる。
State は、gen_server 内部で保持するステータスであり、型はリストでもタプルでも何でも良い。
関数の同期呼び
get() -> gen_server:call(?MODULE, get).
第一引数 ?MODULE は、gen_server の名前。gen_server:start_link/4 で指定した名前を使う。
第二引数 get は、?MODULE に渡したいメッセージ。タプルでも何でも良い。
gen_server:start_link/4 の第一引数に {global, ?MODULE} を指定した場合、下記のようにする。
get() -> gen_server:call({global, ?MODULE}, get).
handle_call(get, _, Count) -> {reply, Count, Count}.
gen_server:call/2 を実行すると handle_call/3 が呼ばれる。
第一引数にメッセージ、第二引数に呼び出し元のプロセスID、第三引数に保持中の State が渡される。
引数を渡す場合は、メッセージをタプルにする。
set(N) -> gen_server:call(?MODULE, {set, N}). handle_call({set, N}, _, _) -> {reply, N, N}.
ちなみに、下記方法で第二引数を表示すると…
test() -> gen_server:call(?MODULE, test). handle_call(test, From, State) -> .io:fwrite("~w~n", [From]), {reply, State, State}.
私の環境では、{<0.29.0>,#Ref<0.0.0.30>} と表示される。
呼び出し元で self/0 を実行した際の出力と比べると、同じ値である事が確認できる。
handle_call は、タプル {reply, Reply, State} を返すようにする。
Reply は、クライアントに送り返される値。
State は、新しいステータス。
ここで {stop, normal, State} を返すと、プロセスは終了する。
stop と normal に関しては後述。
関数の非同期呼び
stop() -> gen_server:cast(?MODULE, stop).
gen_server:call と異なる箇所は、非同期である為、処理の終了を待たず即 ok を返してくる点。
handle_cast(stop, State) -> {stop, normal, State};
gen_server:cast/2 を実行すると handle_cast/2 が呼ばれる。
handle_call/3 と異なり非同期である為、戻り値を返す必要がないので、{noreply, State}. とする。
State は、新しいステータスを設定する。
例では、stop を返して、プロセスを停止させているが…。
停止
handle_cast(stop, State) -> {stop, normal, State};
タプル {stop, normal, State} が戻されると terminate/2 が呼ばれるので、ここでクリーンアップの処理を書く。
terminate(normal, State) -> ok.
疑問点
call と cast の内部処理
get() -> gen_server:call(hoge, fuga).
これ、実行すると hoge ! fuga. を実行しているのかな?
hoge が receive で huga を受け取ると hoge の中の handle_call(fuga, From, State) を実行するのかな?
source 追えば解りそうなので、後で調べる。
start_link と start の停止処理
http://www.erlang.org/doc/design_principles/gen_server.html
英語が得意ではないので、意訳だが…
start_link を使ってる場合、停止の処理は書かなくて良いが、クリーンアップ処理の為に停止をトラップする事はできる。
init(Args) -> ..., process_flag(trap_exit, true), ..., {ok, State}.
と書くと、下記を呼ぶ。
terminate(shutdown, State) -> ..code for cleaning up here.. ok.
start_link ではなく start を使ってる場合は…
... export([stop/0]). ... stop() -> gen_server:cast(ch3, stop). ... handle_cast(stop, State) -> {stop, normal, State}; handle_cast({free, Ch}, State) -> ... ... terminate(normal, State) -> ok.
と言うような事が書かれているが、start_link 使ってても stop を実装する事ができる。
start と start_link の違いと使い分け方って何なんだろうか?
In a Supervision Tree って書いてあった。Stand Alone の時だけ、stop が必要って事か。
handle_info、code_change
具体的に、いつ、どのタイミングで呼ばれるのか不明。後で調べる。
echo server に組み込もうとして悩んでいる点
source を一部抜粋
handle_cast(accept, ListenSocket) -> {ok, Socket} = gen_tcp:accept(ListenSocket), spawn(?MODULE, handle_connection, [Socket]), gen_server:cast(?MODULE, accept), {noreply, ListenSocket};
まず、spawn を使っている箇所だが…、handle_connection 用の別の gen_server を作ろうとしたが下記の理由から断念。
- start や start_link は、内部で register を使って、プロセスに名前を付けている
- register はプロセスの名前として atom を要求する
- atom を動的に生成する方法が不明(扱える最大数が決まっている事は理解してますが…)
次に、handle_cast の中で gen_server:cast(?MODULE, accept) を使っている箇所。これは動かない。
handle_cast から別の関数を呼び、それを再帰でループさせるべきなのだろうか?
最後に、一番最初の gen_tcp:accept(ListenSocket) の行。accept でダンマリになるので、stop 等が呼ばれも受け付けなくなる。
どうしたもんか…。
参考URI
http://www.erlang.org/doc/design_principles/gen_server.html
http://www.erlang.org/doc/man/gen_server.html
Erlang/2006/03/25/side-effect
放牧日記 - dictのサーバ化 其の弐
解った事
httpd の source を読めば解決しそうな気がしてきた
erl_syntax:atom/1 で文字列を atom にするのってアリなんだろうか?
httpd では、supervisor:start_link に渡す引数を作る為に list_to_atom 使ってた。なるほどね
その他
突っ込み大歓迎