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 等が呼ばれも受け付けなくなる。
どうしたもんか…。

解った事

httpd の source を読めば解決しそうな気がしてきた
erl_syntax:atom/1 で文字列を atom にするのってアリなんだろうか?
httpd では、supervisor:start_link に渡す引数を作る為に list_to_atom 使ってた。なるほどね

その他

突っ込み大歓迎