17
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Elixir: GenServerという機能

Last updated at Posted at 2016-04-26

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_callhandle_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_callhandle_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_callhandle_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

17
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?