この章は、gen_server(3)をプログラムに組み込む場合は読むべきです。本章ではすべてのインタフェース関数、およびコールバック関数の詳細について説明しています。
クライアント・サーバモデルの特徴は、中央のサーバと、任意の数のクライアントで構成される、という点です。クライアント・サーバモデルが良く使われるのは、複数の異なるクライアントで共通のリソースを共有したい場面で、そのリソースの管理を行うという場面です。この場合、サーバはリソースの管理の責任を負います。
クライアントサーバモデル
概要 のページでは、純粋なErlangのみで実装したシンプルなサーバのサンプルを出しましたが、ここではそれをgen_serverを利用して書き換えたものを提示します。以下のコードがコールバックモジュールになります:
-module(ch3).
-behaviour(gen_server).
-export([start_link/0]).
-export([alloc/0, free/1]).
-export([init/1, handle_call/3, handle_cast/2]).
start_link() ->
gen_server:start_link({local, ch3}, ch3, [], []).
alloc() ->
gen_server:call(ch3, alloc).
free(Ch) ->
gen_server:cast(ch3, {free, Ch}).
init(_Args) ->
{ok, channels()}.
handle_call(alloc, _From, Chs) ->
{Ch, Chs2} = alloc(Chs),
{reply, Ch, Chs2}.
handle_cast({free, Ch}, Chs) ->
Chs2 = free(Ch, Chs),
{noreply, Chs2}.
このコードについては、これから先のセクションで説明していきます。
上で挙げたサンプルでは、ch3:start_link()を呼ぶことによってgen_serverを起動しています:
start_link() ->
gen_server:start_link({local, ch3}, ch3, [], []) => {ok, Pid}
start_link 関数は、中で gen_server:start_link/4() を呼んでいます。この関数は、新しいプロセスを作成して、gen_serverに結び付けます。
最初の引数の {local, ch3} では名前を設定しています。この場合は、 ch3 という名前で、ローカルなgen_serverが登録されます。
もしも名前が省略された場合には、gen_serverは登録されません。代わりにpidが使用されなければなりません。 {global, 名前} という形式でも名前を設定できます。この場合は register_name/2 を使用してgen_serverが登録されます。
2つ目の引数の ch3 はコールバックモジュールの名前です。コールバックモジュールには、コールバック関数を定義します。
この場合は、インタフェース関数(start_link, alloc, free)はコールバック関数(init, handle_call, handle_cast)と同じモジュールに置かれています。この方法は良く使用されます。一つのプロセスに関連するコードを一つのモジュールにまとめる、良いプラクティスです。
名前の登録が成功した場合には、新しいgen_serverプロセスは、コールバック関数の ch3:init([]) を呼びます。initは {ok, State} を返すことを期待されています。 State はgen_serverの内部状態を表します。この場合は、``State`` は利用可能なチャンネルになります。
init(_Args) ->
{ok, channels()}.
gen_server:start_linkは同期実行されます。gen_serverが初期化されて、リクエストを受け取る用意ができるまでは関数から戻ることはありません。
もしもgen_serverが、スーパバイザとして実行されるなど、管理ツリーの一部として使用される場合には、 gen_server:start_link() を使用すべきです。管理ツリーの一部としては実行されないで、スタンドアローンのgen_serverとして実行される場合には、もう一つの関数の gen_server:start() を使用してください。
同期リクエストの alloc() は、 gen_server:call/2() を使用して実装されています。
ch3はgen_serverの名前で、スタート時に使用した名前と一致する必要があります。 alloc が実際のリクエストになります。
このリクエストからメッセージが作成されて、gen_serverに送信されます。リクエストを受信すると、gen_serverは handle_call(Request, From, State)() を呼び出します。この関数は {reply, Reply, State1} というタプルを返すことが期待されています。 Reply はクライアントに送信し返す返事を表します。 State1 はget_serverの状態を表す新しい値になります。
handle_call(alloc, _From, Chs) ->
{Ch, Chs2} = alloc(Chs),
{reply, Ch, Chs2}.
このコードの場合は、 Ch という、割り当てられたチャンネルを返し、取得可能なチャンネルの残りを現す Chs2 の集合が新しい状態になります。
これにより、上記の場合、 ch3:alloc() は割り当て済みのチャンネル Ch を返し、gen_serverは新しいリクエストを待ちます。また、取得可能なチャンネルのリストが更新されます。
非同期のリクエストである free(Ch) は、 gen_server:cast/2() を利用して実装されています:
free(Ch) ->
gen_server:cast(ch3, {free, Ch}).
ch3 はgen_serverの名前になります。 {free, Ch} というのが実際のリクエストになります。
このリクエストからメッセージが作成されて、gen_serverに送信されます。 cast と、当然のことながら free の両方の関数は ok を返します。
リクエストを受信すると、gen_serverは handle_cast(Request, State)() を呼び出します。この関数は {noreply, State1} というタプルを返すことを期待されています。 State1 はgen_serverの新しい状態値になります。
handle_cast({free, Ch}, Chs) ->
Chs2 = free(Ch, Chs),
{noreply, Chs2}.
この場合、取得可能な更新されたチャンネルのリスト Chs2 が新しい状態となります。今、gen_serverは新しいリクエストを受け取る準備が整いました。
もしもgen_serverが監視ツリーの一部となっている場合には、stop関数を作る必要はありません。gen_serverは監視ツリーによって、自動的に停止させられます。正確には、スーパバイザの中に、 shutdown_strategy 集を定義する必要があります。
もしも終了の前に色々片づけを行う必要がある場合には、シャットダウン戦略にタイムアウト値を設定し、gen_serverがinit()関数の中で終了シグナルを捕まえるようにしなければなりません。終了の命令があったときに、gen_serverは terminal(shutdown, State) という終了関数を呼び出します。
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(ch3, stop).
...
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast({free, Ch}, State) ->
....
...
terminate(normal, State) ->
ok.
stopリクエストを扱うコールバック関数は、 {stop, normal, State1} というタプルを返します。 normal は通常終了、 State1 はgen_serverの状態を表す新しい値です。これにより、 gen_serverは terminate(normal, State1) を呼び出し、奥ゆかしく終了させます。
もしもgen_serverが、リクエストではなくて他のメッセージを受信できるようにする場合には、 handle_info(Info, State)() というコールバック関数を実装する必要があります。他のメッセージの例としては、終了などがありますが、gen_serverが他のプロセス(スーパバイザではなく)とリンクしていて、そこから終了のシグナルを受け取る、という場面が想定されます:
handle_info({'EXIT', Pid, Reason}, State) ->
.. 終了フラグを取り扱うコードをここに書きます ..
{noreply, State1}.
Copyright (c) 1991-2009 Ericsson AB