LoginSignup
9
4

Elixir/GenServerの内部の動きってどうなっているだ?

Last updated at Posted at 2023-04-22

はじめに

GenServerの動作ってどうなっているのか、と元のErlangのコードを調べてみて、その内容と「GenServerって何だ」との自分になり理解した事を記述します。

GenServer(Generic Server)って何

読み込んだり、書き込んだり、また、外部に通知する機能を持ちデータを管理するサーバー(データを提供する人)との事。
そのサーバーの機能を一般化(generic)し、使い易いようにしたもので、上記3つの機能をapp側で作る必要がないようになっている。
ただ、app側から見るとcallback関数ばかりとなり、動きがよくわからず、戸惑う事が多い気がする。
実際には

defmodule Stack do
  use GenServer

  - - -
end

上記Stackモジュールには隠れているタスク(init_it())とAPI関数の処理があり、そこからcallbackされる事により動作している。use GenServerの定義により、そのモジュール自体(Stack)がGenServerの機能を持つ事となる。
データの型(state)はなんでも良く、各callback関数内で更新される事となる。逆にcallback関数内で型を随時変更可能となる。データはどこかに保存されるものでなく、関数の引数として、保持されている。

GenServerの関数

主な関数として以下のものがある。

  • 起動:GenServer.start_link(module, init_arg, options \ [])
      GenServerのタスクを起動し、init()をcallbackする
  • データ取得(返信あり):GenServer.call(server, request, timeout \ 5000)
     上記タスクにメッセージを送信し、handle_call が呼ばれ、応答待ち解放により戻る
  • データ更新(返信なし):GenServer.cast(server, request)
    上記タスクにメッセージを送信し、handle_castが呼ばれる
  • 情報通知(GenServerへの情報通知) handle_info()
    上記タスクがメッセージを受けた時にcallbackを呼ぶ

GenServerの使用例

defmodule Stack do
  use GenServer

  # Client ラッパー
  def start_link(state) when is_list(state) do
    GenServer.start_link(__MODULE__, state, name: :stack) # pidを:stackに名付ける
  end
  def push(element) do  # pid=:stack
    GenServer.cast(:stack, {:push, element})
  end
  def pop() do          # pid=:stack
    GenServer.call(:stack, :pop)
  end

  # Server (callbacks)
  @impl true
  def init(stack) do    # start_link()のcallback
    {:ok, stack}
  end
  @impl true
  def handle_call(:pop, _from, [head | tail]) do   # GenServer.callのcallback
    {:reply, head, tail}
  end
  @impl true
  def handle_cast({:push, element}, state) do   # GenServer.castのcallback
    {:noreply, [element | state]}
  end
end

ラッパー関数を準備するのが一般的

次のように直接GenServerを呼べば動作する。

iex> GenServer.start_link(Stack, [], name: :stack)
{:ok, #PID<0.153.0>}
iex> GenServer.cast(:stack, {:push, "first"})
:ok
iex> GenServer.call(:stack. :pop)
"first"

このように直接でも問題はないが、一般的にラッパー関数(上記例、start_link(),push(),pop())を準備する事が多い。
ちなみに、cast()、call()はpidを渡すが、ここでは起動時にname: :stackの設定より、pidと:stackが関連付けられ、:stackが使用できるようにしている。

個人的には違うNodeからremoteで使用する事が多いので、あまりこのラッパーを準備する事はしていない。

start_link()の動き

  1. start_link(module, init_arg, option)によりmodule内にGenServerの機能が準備される。
  2. 内部のタスクinit_it()を起動する。その時のinit_argが初期のデータ(state)となる。
  3. 起動された内部のタスクからcallback関数、init()が呼ばれる。引数はstateとなり、データを渡す。
  4. init()callback関数から戻る時に{:ok, state}とデータが戻り、データ更新される。このinit()関数は無くても、defaultとして準備されている。
  5. init()が呼ばれ、内部のタスクの処理が終了した時点で受信応答待ちとなっているapiにackとしてmsg送信する。
  6. start_link()のreturnとして、{:ok, pid}を戻す事により、内部のタスクのpidがわかる。

call()の動き

  1. call(pid<server>, request, timeout)によりapi関数に通知する。
  2. 内部タスクにrequestのmsgを送信する。この時、$gen_callの情報付加し、内部タスクがcall関数である事を認識する。そして、api関数内で応答待ち(receive)となる。
  3. 内部タスクからcallback関数、handle_call()を呼ぶ。引数 (request, from, state)、このrequest情報により、どのcallbackなのか、判定されている。
  4. 戻り、{:reply, reply, new_state}、により応答送信、戻り値、データ更新する。
  5. api関数内で応答待ちに対して、応答データ(reply)とともにmsg送信する
  6. api関数はデータ(reply)を戻す。

cast()の動き

  1. cast(pid<server>, request)により情報をapi関数に渡す
  2. 内部タスク(pid)に対して、request$gen_castを付加したmsgを送信する。
  3. castは応答待ちをせず、そのまま戻る。
  4. 内部タスクはmsgを受けるとcallback関数、handle_cast(){requst, state}で呼ぶ、requestの情報によりどのhandle_cast()かが判定される。
  5. callback関数から戻り{:noreply,new_state}により新しいデータを更新する。

情報通知(GenServerへの情報通知)の動き

  1. callback関数から内部タスク(pid)にmsg送信する。
  2. 他の処理から内部タスク(pid)にmsg送信する。
  3. 内部タスクはcallback関数、handle_info()をmsg受信により呼ぶ。{msg, state}
  4. handle_info()からの戻り、{:noreply, new_state}によりデータ更新する。

callback関数内でのreceiveのようなwait処理は禁止

内部のタスク内からcallback関数が呼ばれているので、関数内でwait処理があるとGenServerの動きが壊される。そのために、callback関数は素直に戻る必要がある。
GenServer.start_link()、call()などの関数内でwaitしているものがあるので、その点注意が必要となる。

まとめ

これまで、正常動作の処理を記述したが、エラー処理やtimeout処理、そして、start()、start_link()の違いなどのoptionよる動作があり、複雑に絡んでいる。
erlangのコードを見ていた結果、理解するのに困難な代物で、二度と見たくないと思わされるものだった。(感想)

この資料、GenServerを使う上での参考になれば幸いです。

9
4
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
9
4