GenServerとは
プログラムを書いていると, 毎回とは言わないものの頻繁に出てくる設計のパターンに出くわすことが良くあります.
いわゆるデザインパターンというやつですね.
Elixirでは, 様々パターンを実装する上で, 全部開発者が1から実装するのではなく,
そのパターンを半分だけ実装した状態のフレームワークを提供し, 開発者は対応するBehaviourを実装したモジュールを用意するだけで, パターンに基づいた実装を完成させることができる仕組みが用意されています.
(手前味噌ですが, Behaviourについての解説はこちらを御覧ください)
GenServerも, そういったElixirによって用意されたフレームワークの一つです.
では, どんな設計パターンを, フレームワーク化しているのでしょうか?
それは
クライアントプロセスから何らか形式のメッセージを受け取り, メッセージと状態を元に何らかの処理を行い, 結果を返す(もしくは返さない)
という, 極めて単純で一般的な設計パターンです.
GenSeverの使い方
公式ドキュメントのサンプルコードを使いながら GenServerの使い方を説明していきます.
defmodule Stack do
use GenServer
# Callbacks
def handle_call(:pop, _from, [h|t]) do
{:reply, h, t}
end
def handle_cast({:push, item}, state) do
{:noreply, [item|state]}
end
end
# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])
# This is the client
GenServer.call(pid, :pop)
# => :hello
GenServer.cast(pid, {:push, :world})
# => :ok
GenServer.call(pid, :pop)
# => :world
用意すべきサーバプロセス用のモジュールは, まずGenServerモジュール(=Behaviour)をuseし
defmodule Stack do
use GenServer
GenServerフレームワークから呼び出される(Callbackされる)関数を実装していきます.
GenServerモジュールで定義されているCallback関数は6つありますが, ここで実装されているのは
handle_call
とhandle_cast
のみで, それ以外の関数はデフォルトの挙動のままにしています.
def handle_call(:pop, _from, [h|t]) do
{:reply, h, t}
end
def handle_cast({:push, item}, state) do
{:noreply, [item|state]}
end
end
handle_call
は, クライアントからの送られてきたメッセージ, クライアントプロセスのpid, サーバプロセスの現在の状態を受け取り, {:reply/返り値/新しい状態}
というタプルを返します.
返り値のタプルに新しい状態が含まれていることからわかるように, 次にこの関数が呼び出されるときは
引数の状態にはこの新しい状態が渡されます.
handle_cast
は, クライアントからの送られてきたメッセージ, サーバプロセスの現在の状態を受け取り, {:noreply/新しい状態}
というタプルを返します.
handle_call
とhandle_cast
の違いは, クライアントに対して値を返すか否かであり, handle_call
は値を返し, handle_cast
は値を返しません.
このモジュールをサーバプロセスとして利用するやり方は, まずGenServerモジュールのstart関数を使ってプロセスを立ち上げ
# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])
あとは, 生成されたpidを使って, Stackの値が欲しい時はGenServer.call
を, Stackに値を積みたいときはGenServer.cast
を使ってメッセージを送っていきます.
# This is the client
GenServer.call(pid, :pop)
# => :hello
GenServer.cast(pid, {:push, :world})
# => :ok
GenServer.call(pid, :pop)
# => :world
プロセス間で同期を取りながらメッセージをやり取りするには, まずサーバ側をrecieve状態にして
クライアントはメッセージをsendしたら, サーバからのメッセージを受け取るようにreceive状態にして...
と言った処理を書いてやる必要があるのですが, 見ての通りそんな記述は全くありません.
開発者は, ただGenServer.call
経由で呼び出されるhandle_call
や, GenServer.cast
経由で呼び出されるhandle_cast
を実装するだけでいいのです.
(だからこそフレームワークなのですが)
RemoteAgentをGenServerを使って実装し直す
先日作成したRemoteAgentは, クライアントからメッセージを受け取り, 状態を変えたり値を返したりするモジュールでした.
つまり, これはGenServerが提供しているフレームワークの処理の流れそのものということです.
このモジュールをGenServerを使った形に書き換えていきます.
書き換え前のコードは以下のとおり.
defmodule RemoteAgent3 do
@name :agent_register3
def start(init \\ "") do
pid = spawn(RemoteAgent3, :agent, [init])
:global.register_name @name, pid
:ok
end
def agent(state) do
receive do
{client, :get} ->
send client, {:ok, state}
agent(state)
{client, :update, new_state} ->
send client, {:ok, new_state}
agent(new_state)
end
end
def get() do
send :global.whereis_name(@name), {self, :get}
receive do
{:ok, state} -> state
end
end
def update(new_state) do
send :global.whereis_name(@name), {self, :update, new_state}
receive do
{:ok, _} -> :ok
end
end
end
この中で, クライアントからのメッセージを処理しているagent
関数の中では...
def agent(state) do
receive do
{client, :get} ->
send client, {:ok, state}
agent(state)
{client, :update, new_state} ->
send client, {:ok, new_state}
agent(new_state)
end
end
{client, :get}
というメッセージを受け取ったら現在の状態を返し, {{client, :update, new_state}}
というメッセージを受け取ったら, 状態を更新しています.
これをそれぞれhandle_call
とhandle_cast
で書き換えます.
def handle_call(:get, _client, state) do
{:reply, state, state}
end
def handle_cast({:update, new_state}, state) do
{:noreply, new_state}
end
試しにこの状態で動かしてみましょう.
iex(2)> {:ok, pid} = GenServer.start_link(RemoteAgent3, "init message")
{:ok, pid} = GenServer.start_link(RemoteAgent3, "init message")
{:ok, #PID<0.66.0>}
iex(3)> GenServer.call(pid, :get)
GenServer.call(pid, :get)
"init message"
iex(4)> GenServer.cast(pid, {:update, "updated message"})
GenServer.cast(pid, {:update, "updated message"})
:ok
iex(5)> GenServer.call(pid, :get)
GenServer.call(pid, :get)
"updated message"
この時点で機能としては使えていることがわかります.
ただ, 呼び出すたびにGenServer.~~
と書かないといけないのも冗長なので, これらの記述をラップして, もう少し使いやすいインタフェイスを用意しましょう.
まずサーバプロセス開始の関数を用意します.
def start(init \\ "") do
GenServer.start_link(RemoteAgent3, init, name: @name)
:ok
end
次に, 値の取得/更新用にそれぞれget
関数/update
関数を用意します.
def get() do
GenServer.call(@name, :get)
end
def update(new_state) do
GenServer.cast(@name, {:update, new_state})
end
説明が抜けていましたがGenServerの関数を使ってプロセスを立ち上げる際に, 引数として名前を渡してやることで
立ち上がったプロセスにその名前でアクセスすることができるようになります.
ここでは, その機能を使って, callやcastの呼び出しはpidではなくて名前を使ってプロセスを指定するようにしています.
モジュール全体はこんな感じです.
defmodule RemoteAgent3 do
use GenServer
@name :agent_register3
def start(init \\ "") do
GenServer.start_link(RemoteAgent3, init, name: @name)
:ok
end
def get() do
GenServer.call(@name, :get)
end
def update(new_state) do
GenServer.cast(@name, {:update, new_state})
end
# GenServer Callback functions
def handle_call(:get, _client, state) do
{:reply, state, state}
end
def handle_cast({:update, new_state}, state) do
{:noreply, new_state}
end
end
あとはこれらの関数を使っていくだけです.
iex(2)> RemoteAgent3.start("init message")
RemoteAgent3.start("init message")
:ok
iex(3)> RemoteAgent3.get
RemoteAgent3.get
"init message"
iex(4)> RemoteAgent3.update("updated message")
RemoteAgent3.update("updated message")
:ok
iex(5)> RemoteAgent3.get
RemoteAgent3.get
"updated message"
まとめ
GenServerは便利
使えるときは積極的に使っていこう
参考
Elixir GenServer: http://elixir-lang.org/docs/stable/elixir/GenServer.html
Elixir の OTP (GenServer 編): http://qiita.com/naoya@github/items/ae17a8166e52fc463012