■ 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ラッピングから構成されます。それぞれの関数の詳細は後で説明しますが、説明なしでも大体理解できると思います。
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)を使うことが多いようです。
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を関数の直前に置くことで、コンパイルエラーとなります。
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と考えられます。
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側のファイルです。
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
今回は以上です。