LoginSignup
1
1

More than 1 year has passed since last update.

Elixir Concurrent Programming(2) - GenServer

Last updated at Posted at 2018-09-26

■ ElixirのConcurrent Programmingについてまとめた過去記事
Elixir Concurrent Programming(1) - Spawn - Qiita
Elixir Concurrent Programming(2) - GenServer - Qiita
Elixir Concurrent Programming(3) - Supervisors - Qiita
Elixir Concurrent Programming(4) - Task and Agent -Qiita

 spawnで作られたプロセスは、tail recursionの中でreceiveを使い、永続的にクライアントからのリクエストを処理を行うことでサーバの役割を実現することができます。さらに recursionにおいて引数としてstateを持たせることも簡単にできます。

 しかしもっと簡単にstateを持ったサーバを実現するには、OTP GenServer behaviorを使うことです。これはModuleの定義においてuse GenServer行を追加することで可能です。OTPはクライアントからのリクエストに応じて適切なcallbackを呼び出します。例えばcallbackはhandle_call(request,client,state)のような形で呼び出されます。callbackは何らかの処理を行い、stateを更新し、returnを返します。

 OTP behavioursにはGenServerの他に、supervisor(concurrent systemsにおけるエラーリカバリー)やapplication(components や librariesの実装)などがあります。

■ Phoenix LiveView で GenServer を使ってみた(2022/10/23)
東京電力電力供給状況監視 - Phoenix LiveView - Qiita

1.GenServerの定型

1-1.pidでcallする

 
 GenServerのmoduleは、ServerプロセスとclientのインターフェースであるClientラッピングから構成されます。それぞれの関数の詳細は後で説明しますが、説明なしでも大体理解できると思います。

my_server1.ex
defmodule MyServer1 do
  use GenServer    # OTP GenServer behaviorを使う

  ##### (1) Clientラッピング

  def start do
    GenServer.start(MyServer1, nil)
  end

  def put(pid, key, value) do
    GenServer.cast(pid, {:put, key, value})
  end

  def get(pid, key) do
    GenServer.call(pid, {:get, key})
  end


  ##### (2) Serverプロセス

  def init(_) do  # GenServer.startからcallされstateを初期化します
    {:ok, %{}}
  end

  def handle_cast({:put, key, value}, state) do
    {:noreply, Map.put(state, key, value)}
  end

  def handle_call({:get, key}, _, state) do
    {:reply, Map.get(state, key), state}
  end
end

 それではiexで実行してみます。

# iex  my_server1.ex

### serverを起動させてpidを得ます
iex(1)> {:ok, pid} = MyServer1.start()
{:ok, #PID<0.89.0>}

### pidを指定してstateを更新します
iex(2)> MyServer1.put(pid, :my_key, :taro)
:ok

### pidを指定してstateをgetします
iex(3)> MyServer1.get(pid, :my_key)
:taro

### pidを指定してstateを再度更新します
iex(4)> MyServer1.put(pid, :my_key, :jiro)
:ok

### pidを指定してstateをgetします
iex(5)> MyServer1.get(pid, :my_key)
:jiro

###  Clientラッピング(インターフェース)を通さずに直接call
iex(6)>  GenServer.call(pid ,{:get, :my_key})
:jiro

1-2.名前でcallする

 通常はGenServerからcallbackを呼び出すときは、pidで呼び出します。しかしGenServer.startでnameオプションで名前をつけることで、pidの代わりに名前を使うことができます。多くの場合は、名前としてmodule名(MODULE)を使うことが多いようです。

my_server2.ex
defmodule MyServer2 do
  use GenServer

  ##### (1) Clientラッピング

  def start do
    GenServer.start(MyServer2, nil, name: __MODULE__) ### 名前付きでstart
  end

  def put(key, value) do
    GenServer.cast(__MODULE__, {:put, key, value})
  end

  def get(key) do
    GenServer.call(__MODULE__, {:get, key})
  end


  ##### (2) Serverプロセス

  def init(_) do
    {:ok, %{}}
  end

  def handle_cast({:put, key, value}, state) do
    {:noreply, Map.put(state, key, value)}
  end

  def handle_call({:get, key}, _, state) do
    {:reply, Map.get(state, key), state}
  end
end

 それではiexで実行してみます。

# iex  my_server2.ex

### serverを起動させてpidを得ます
iex(1)> {:ok, pid} = MyServer2.start()
{:ok, #PID<0.89.0>}

### pidを指定せずに、stateを更新します
iex(2)> MyServer2.put(:my_key, :taro)
:ok

### pidを指定せずに、stateをgetします
iex(3)> MyServer2.get(:my_key)
:taro

### pidを指定せずに、stateを再度更新します
iex(4)> MyServer2.put(:my_key, :jiro)
:ok

### pidを指定せずに、stateをgetします
iex(5)> MyServer2.get(:my_key)
:jiro

###  Clientラッピング(インターフェース)を通さずに直接call
iex(6)>  GenServer.call(MyServer2 ,{:get, :my_key})
:jiro

2.Callback関数

 以下にGenServerのcallback関数を説明します。

2-1.init(start_arguments)

 新サーバを作成する時にGenServerのstart_link(MyServer, start_arguments)によってcallされます。引数が渡されます。initの通常の処理は state = start_arguments で初期化することです。

return値:
{:ok, state} <- 成功時 
{:stop, reason} <- start失敗時
{:ok, state, timeout} 
  <- timeout時間内にmessageが無ければGenServerが:timeout messageを送る

2-2.handle_call(request, from, state)

 クライアント側がGenServer.call(pid, request)を実行したときに呼ばれるcallbackです。引数のrequestはクライアントからのリクエストです。from = クライアントのpidです。stateはGenServerが保持している状態です。ちなみに一般的にcallという単語は同期呼び出しに使い、castは非同期呼び出しに使われます。

{:reply, result, new_state} <- 成功時
{:stop, reason, new_state}  <- Severプロセスを停止したとき
{:stop, reason, response, new_state} <-Serverプロセスの停止前に通知が必要なとき

2-3.handle_cast(request, state)

 クライアント側がGenServer.cast(pid, request)を実行したときに呼ばれるcallbackです。クライアント側はreplyを期待していません。

{:noreply, new_state} <- 成功時
{:stop, reason,new_state}  <- Severプロセスを停止したとき

2-4.handle_info(info, state)

 callやcast以外のmessageを処理します。例えばtimeout messagesや、linked processesからのtermination messagesがここで処理されます。またGenServerを遠さずにpid指定でsendされたmessageもここで処理されます。

2-5.terminate(reason, state)

 Serverプロセスが停止しそうなときに呼ばれます。しかしsupervisionを使うことでここでの処理は必要なくなります。

2-6.code_change(from_version, state, extra)

 システム更新(コード変更)が行われても、Serverは停止することがありません。このcallbackはシステム更新があったときに呼ばれ、更新の事実を確認することができます。

2-7.format_status(reason, [pdict, state])

stateを表示するフォーマットを変更します。

3.デバッグ

3-1.[debug: [:trace]]オプション

 GenServer.startで**[debug: [:trace]]**を指定することで、実行時にServerプロセスのstateをtraceすることができます。

# iex  my_server1.ex
iex(1)> {:ok,pid} = GenServer.start(MyServer1, nil, [debug: [:trace]])
{:ok, #PID<0.89.0>}
iex(2)> MyServer1.put(pid, :my_key, :taro)
*DBG* <0.89.0> got cast {put,my_key,taro}
*DBG* <0.89.0> new state #{my_key => taro}
:ok
iex(3)> MyServer1.get(pid, :my_key)
*DBG* <0.89.0> got call {get,my_key} from <0.87.0>
*DBG* <0.89.0> sent taro to <0.87.0>, new state #{my_key => taro}
:taro
iex(4)> MyServer1.put(pid, :my_key, :jiro)
*DBG* <0.89.0> got cast {put,my_key,jiro}
*DBG* <0.89.0> new state #{my_key => jiro}
:ok

3-2.[debug: [:statistics]]オプション

 GenServer.startで**[debug: [:statistics]]を指定することで、実行時にServerプロセスの簡単な統計を取ることができます。:sys.statistics**で確認できます。

# iex  my_server1.ex
iex(1)> {:ok,pid} = GenServer.start(MyServer1, nil, [debug: [:statistics]])
{:ok, #PID<0.89.0>}
iex(2)> MyServer1.put(pid, :my_key, :taro)
:ok
iex(3)> MyServer1.get(pid, :my_key)
:taro
iex(4)> MyServer1.put(pid, :my_key, :jiro)
:ok
iex(5)> :sys.statistics pid, :get
{:ok,
 [
   start_time: {{2018, 9, 26}, {10, 52, 17}},
   current_time: {{2018, 9, 26}, {10, 53, 2}},
   reductions: 107,
   messages_in: 3,
   messages_out: 0
 ]}

3-3.:sys.trace 関数

 また別の方法ですが、:sys.traceでtraceをon/offできます。さらに**:sys.get_status**で指定したpidのプロセスのstatusを確認することができます。

# iex  my_server1.ex
iex(1)> {:ok, pid} = MyServer1.start() # [debug: [:trace]]は不要です。
{:ok, #PID<0.89.0>}
iex(2)> :sys.trace pid, true   # traceをonにします。
:ok
iex(3)> MyServer1.put(pid, :my_key, :taro)  
*DBG* <0.89.0> got cast {put,my_key,taro}
*DBG* <0.89.0> new state #{my_key => taro}
:ok
iex(4)> MyServer1.get(pid, :my_key)
*DBG* <0.89.0> got call {get,my_key} from <0.87.0>
*DBG* <0.89.0> sent taro to <0.87.0>, new state #{my_key => taro}
:taro
iex(5)> MyServer1.put(pid, :my_key, :jiro)
*DBG* <0.89.0> got cast {put,my_key,jiro}
*DBG* <0.89.0> new state #{my_key => jiro}
:ok
iex(6)> :sys.trace pid, false   # traceをoffにします。
:ok
iex(7)> MyServer1.get(pid, :my_key)
:jiro
iex(8)> :sys.get_status pid     # プロセスpidのstatusを確認
{:status, #PID<0.89.0>, {:module, :gen_server},
 [
   [
     "$initial_call": {MyServer1, :init, 1},
     "$ancestors": [#PID<0.87.0>, #PID<0.57.0>]
   ],
   :running,
   #PID<0.89.0>,
   [],
   [
     header: 'Status for generic server <0.89.0>',
     data: [
       {'Status', :running},
       {'Parent', #PID<0.89.0>},
       {'Logged events', []}
     ],
     data: [{'State', %{my_key: :jiro}}]
   ]
 ]}

3-4.コンパイルチェック @impl GenServer

 my_server3.exをkピーしてmy_server3.exをつくり、以下のhandle_callの引数を削除します。この修正は誤りでありますが、普通にiex my_server3.exでコンパイルしたときはエラーは生じません。 @impl GenServerを関数の直前に置くことで、コンパイルエラーとなります。

my_server3.ex
defmodule MyServer3 do
#
  @impl GenServer
  def handle_call({:get, key},  state) do
  #def handle_call({:get, key}, _, state) do
#
end

 それではiexを立ち上げてみましょう。

#iex  my_server3.ex
Erlang/OTP 20 [erts-9.1] [source] [64-bit] [smp:3:3] [ds:3:3:10] [async-threads:10] [hipe] [kernel-poll:false]

warning: got "@impl GenServer" for function handle_call/2 but this behaviour does not specify such callback. The known callbacks are:

  * GenServer.code_change/3 (function)
  * GenServer.format_status/2 (function)
  * GenServer.handle_call/3 (function)
  * GenServer.handle_cast/2 (function)
  * GenServer.handle_info/2 (function)
  * GenServer.init/1 (function)
  * GenServer.terminate/2 (function)

  my_server3.ex:31

Interactive Elixir (1.7.0-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

4.プロジェクトの中でComponent化する

 Elixirのプロジェクトを作成し、今まで見てきたmy_server1.exをプロジェクトの中におくことにします。

mix new mymap
cd  mymap
mkdir lib/mymap

 my_server1.exのプログラムコードをcomponent化します。つまり(1) Clientラッピングの部分と、(2) Serverプロセスの部分で、2ファイルに分割します。lib/mymap.exとlib/mymap/server.exに分けます。

 client側のファイルです。これはServerへのAPIと考えられます。

lib/mymap.ex
defmodule Mymap do

  @server Mymap.Server   # module attributesで名前を定義します。

  ##### (1) Clientラッピング

  def start_link do
    GenServer.start_link(@server, nil, name: @server) ### 名前付きでstart
  end

  def put(key, value) do
    GenServer.cast(@server, {:put, key, value})
  end

  def get(key) do
    GenServer.call(@server, {:get, key})
  end

 Server側のファイルです。

lib/mymap/server.ex
defmodule Mymap.Server do
  use GenServer

  ##### (2) Serverプロセス

  def init(_) do
    {:ok, %{}}
  end

  def handle_cast({:put, key, value}, state) do
    {:noreply, Map.put(state, key, value)}
  end

  def handle_call({:get, key}, _, state) do
    {:reply, Map.get(state, key), state}
  end
end

 それでは実行してみましょう。

# iex -S mix
iex(1)> Mymap.start_link
{:ok, #PID<0.120.0>}
iex(2)> Mymap.put(:my_key, :taro)
:ok
iex(3)> Mymap.get(:my_key)
:taro
iex(4)> Mymap.put(:my_key, :jiro)
:ok
iex(5)> Mymap.get(:my_key)
:jiro

今回は以上です。

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