Ge

GenServer の Elixir

4個の演習

GenServerについて

GenServer(汎用サーバー)は、Elixirのプロセス間でよく行われるクライアントとサーバーのやり取りを抽象化するビヘイビアです。

プロセスについて学んだときの受信ループを覚えていますか? GenServerビヘイビアは、こうしたループを実装したり、そのようなループを実行するプロセスとメッセージをやり取りしたりするための抽象化を提供します。状態を保持したり、非同期コードを実行したりするのが簡単になります。

Note

GenServerという名前は、いくつもの意味で使われるので注意してください。GenServerビヘイビアを_使う_モジュールを指すこともあれば、GenServerビヘイビアを_使う_モジュールから起動された_プロセス_を指すこともあります。

GenServerビヘイビアは、必須のコールバックであるinit/1と、いくつかの興味深い任意のコールバック、handle_call/3、handle_cast/2、handle_info/3を定義します。GenServerを利用する_クライアント_は、こうしたコールバックを直接呼び出すことは想定されていません。代わりに、GenServerモジュールが提供する関数を使って、クライアントはGenServerプロセスとやり取りします。

多くの場合、1つのモジュールが、クライアントAPI(Elixirアプリの他の部分がこのGenServerプロセスとやり取りするために呼び出せる関数群)と、サーバーコールバックの実装(このGenServerのロジックを含むもの)の両方を定義します。

まずはGenServerの簡単な例を見て、それから各コールバックの意味を学びましょう。

例

これは、長いドライブの途中でうるさい同乗者に何度も尋ねられる質問、つまり「もう着いた?」に応答するサーバーの例です。この質問が何回されたかを記録し、だんだんとイライラした返事を返します。

defmodule AnnoyingPassengerAutoresponder do
  use GenServer
  # Client API

  def start_link(init_arg) do
    GenServer.start_link(__MODULE__, init_arg)
  end

  def are_we_there_yet?(pid) do
    GenServer.call(pid, :are_we_there_yet?)
  end

  # Server callbacks

  @impl GenServer
  def init(_init_arg) do
    # the initial count of questions asked is always 0
    state = 0
    {:ok, state}
  end

  @impl GenServer
  def handle_call(:are_we_there_yet?, _from, state) do
    reply =
      cond do
        state <= 3 -> "No."
        state <= 10 -> "I told you #{state} times already. No."
        true -> "..."
      end

    # increase the count of questions asked
    new_state = state + 1
    # reply to the caller
    {:reply, reply, new_state}
  end
end

コールバック

init/1

サーバーは、GenServer.start/3またはGenServer.start_link/3を呼び出すことで起動できます。これらの関数の違いについては、リンクの概念で学びました。

これらの2つの関数は、次のとおりです。

  • 第一引数として、GenServerビヘイビアを実装したモジュールを受け取ります。
  • init_argと呼ばれる第二引数として、任意の値を受け取ります。名前のとおり、この引数はinit/1コールバックに渡されます。
  • 省略可能な第三引数として、プロセスの実行に関する高度なオプションを受け取ります。ここでは扱いません。

GenServer.start/3またはGenServer.start_link/3を呼び出してサーバーを起動すると、init/1コールバックがブロッキングな方法で呼び出されます。init/1の戻り値によって、サーバーを正しく起動できるかどうかが決まります。

init/1コールバックは、通常、次のいずれかの値を返します。

  • {:ok, state}。サーバーはstateを初期状態として受信ループを開始します。stateはどんな型でもかまいません。
  • {:stop, reason}。reasonはどんな型でもかまいません。サーバーは受信ループを開始しません。プロセスは与えられたreasonで終了します。

ここでは扱わない、より高度な方法もあります。

サーバーの受信ループが開始されると、GenServer.start/3とGenServer.start_link/3は{:ok, pid}というタプルを返します。そうでなければ、{:error, reason}を返します。

handle_call/3

返信を必要とするメッセージは、GenServer.call/2でサーバープロセスに送ることができます。この関数は、第一引数に実行中のサーバープロセスのpid、第二引数にメッセージを取ります。メッセージはどんな型でもかまいません。

handle_call/3コールバックは、同期メッセージを処理して応答する役割を担います。3つの引数を受け取ります。

  1. message:GenServer.call/2の第二引数として渡された値です。
  2. from:GenServer.call/2を呼び出しているプロセスのpidです。多くの場合、この引数は無視できます。
  3. state:サーバーの現在の状態です。初期値はinit/1コールバックで設定されたことを思い出してください。

handle_call/3コールバックは、通常、{:reply, reply, state}という3要素のタプルを返します。これは、タプルの第二要素であるreply(どんな型でもかまいません)が呼び出し元に返されることを意味します。タプルの第三要素であるstateは、このメッセージを処理したあとのサーバーの新しい状態です。

ここでは扱わない、より高度な方法もあります。

Note

このコールバックの動作を名前から覚えるには、電話で誰かを「呼び出す(call)」ことを思い浮かべてください。

相手が電話に出られるなら、すぐに(同期的に)返事が返ってきます。

handle_cast/2

返信を必要としないメッセージは、GenServer.cast/2でサーバープロセスに送ることができます。引数はGenServer.call/2と同じです。

handle_cast/2コールバックは、こうしたメッセージを処理する役割を担います。messageとstateの2つの引数を受け取りますが、これはhandle_call/3コールバックと同じ引数です(fromを除く)。

handle_cast/2コールバックは、通常、{:noreply, state}という2要素のタプルを返します。

ここでは扱わない、より高度な方法もあります。

Note

このコールバックの動作を名前から覚えるには、「cast」には「投げる(throw)」という意味もあることを思い出してください。

メッセージを瓶に入れて海に投げ込んだら、すぐに返事が返ってくるとは期待しません。もしかすると、永遠に返事は来ないかもしれません。

callとcastのどちらを使うべき?

クライアントコードがサーバーからの返信を必要としない場合でも、ほとんどの場合callを使います。

callを使うと返信を待つため、これがバックプレッシャーの仕組みとして働きます(クライアントが一度に多くのメッセージを送りすぎるのを防ぎます)。また、サーバーがクライアントのメッセージを受け取って処理したことを確かめる唯一の方法は、サーバーからの返信を受け取ることです。

handle_info/2

メッセージは、GenServer.call/2やGenServer.cast/2を呼び出す以外の方法でもサーバーのメールボックスに届くことがあります。たとえば、通常のsend/2関数を呼び出す場合です。

こうしたメッセージを処理するには、handle_info/2コールバックを使います。このコールバックはhandle_cast/2とまったく同じように動作します。

GenServerビヘイビアは、handle_info/2の、予期しないメッセージに関するエラーをログに記録するすべてを受け止める実装を提供します。このデフォルト実装を上書きする場合は、自分自身ですべてを受け止める実装を必ず用意してください。忘れると、予期しないメッセージを受け取ったときにサーバーがクラッシュします。

タイムアウト

上で説明した4つのコールバックの戻り値は、タプルの要素をもう1つ、タイムアウトを加えて拡張できます。たとえば、init/1から{:ok, state}を返す代わりに、{:ok, state, timeout}を返します。

タイムアウトは、一定期間メールボックスにメッセージが届かないことを検出するために使えます。サーバーがコールバックのいずれかからタイムアウトを返し、指定したミリ秒数が経過してもメッセージが届かなければ、handle_info/2が第一引数に:timeoutを渡して呼び出されます。

GitHubで編集 リンクは新しいウィンドウまたはタブで開きます

GenServerを学習する

練習はロックされています

GenServerを練習するには、あと2個の演習のロックを解除してください