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 行程溝通。

通常,一個模組會同時定義_用戶端 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 來啟動。我們在連結這個概念中介紹過這兩個函式的差異。

這兩個函式:

  • 第一個引數接受一個實作 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 可以是任何型別。伺服器不會開始執行接收迴圈。行程會以給定的原因結束。

還有一些更進階的可能性,我們現在不會涵蓋。

如果伺服器的接收迴圈順利啟動,GenServer.start/3 和 GenServer.start_link/3 會回傳 {:ok, pid} 元組。否則會回傳 {:error, reason}

handle_call/3

需要回覆的訊息可以透過 GenServer.call/2 傳送給伺服器行程。這個函式的第一個引數是執行中伺服器行程的 pid,第二個引數是訊息。訊息可以是任何型別。

handle_call/3 回呼負責處理同步訊息並做出回應。它會收到三個引數:

  1. message:傳給 GenServer.call/2 第二個引數的值。
  2. from:呼叫 GenServer.call/2 的行程 pid。這個引數通常可以忽略。
  3. state:伺服器目前的狀態。還記得它的初始值是在 init/1 回呼中設定的。

handle_call/3 回呼通常會回傳一個三元素的元組 {:reply, reply, state}。這表示元組中的第二個元素 reply(可以是任何型別)會回傳給呼叫方。元組中的第三個元素 state,是伺服器處理完這則訊息後的新狀態。

還有一些更進階的可能性,我們現在不會涵蓋。

Note

想靠名稱記住這個回呼的功能, 就想像你打電話「呼叫」某人。

如果對方有空,你馬上就會收到回覆(同步地)。

handle_cast/2

不需要回覆的訊息可以透過 GenServer.cast/2 傳送給伺服器行程。它的引數和 GenServer.call/2 相同。

handle_cast/2 回呼負責處理這些訊息。它會收到兩個引數 message 和 state,和 handle_call/3 回呼的引數相同(差別在於沒有 from)。

handle_cast/2 回呼通常會回傳一個二元素的元組 {:noreply, state}。

還有一些更進階的可能性,我們現在不會涵蓋。

Note

想靠名稱記住這個回呼的功能, 記得「to cast」也有「丟擲」的意思。

如果你把一封瓶中信丟進海裡, 就不會期待馬上收到回覆, 或者根本永遠不會收到。

我該用 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 的通用實作,會記錄非預期訊息的錯誤。如果你覆寫了那個預設實作,請務必自己加上能攔截所有訊息的實作。否則一旦伺服器收到非預期的訊息,就會崩潰。

逾時

上面四個回呼的回傳值,都可以再加上一個元組元素:逾時。例如,init/1 可以不回傳 {:ok, state},而是回傳 {:ok, state, timeout}。

逾時可以用來偵測信箱中在某段時間內都沒有訊息。如果伺服器從某個回呼回傳逾時,而且在指定的毫秒數內都沒有訊息送達,就會呼叫 handle_info/2,並以 :timeout 作為第一個引數。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 GenServer

練習已鎖定

再解鎖 2 個練習,就能練習 GenServer