檢視原始碼 GenServer 行為 (Elixir v1.16.2)

用於實作客戶端伺服器關係伺服器的行為模組。

GenServer 是與其他 Elixir 程序相同的程序,可用於保留狀態、非同步執行程式碼等等。使用這個模組實作的通用伺服器程序 (GenServer) 的優點是,它將具有一組標準介面函數,並包含用於追蹤和錯誤回報的功能。它也會融入監督樹狀結構。

graph BT
    C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
    A & B & C -->|request| GenServer
    GenServer -.->|reply| A & B & C

範例

GenServer 行為抽象出常見的客戶端伺服器互動。開發人員只需要實作他們有興趣的回呼和功能。

讓我們從一個程式碼範例開始,然後探討可用的回呼。假設我們想實作一個使用 GenServer 的服務,它可以像堆疊一樣運作,讓我們可以推入和彈出元素。我們將透過實作三個回呼,使用我們自己的模組自訂一個通用 GenServer。

init/1 將我們的初始參數轉換為 GenServer 的初始狀態。 handle_call/3 在伺服器收到同步 pop 訊息時觸發,從堆疊中彈出一個元素並將其傳回給使用者。 handle_cast/2 在伺服器收到非同步 push 訊息時觸發,將一個元素推入堆疊中。

defmodule Stack do
  use GenServer

  # Callbacks

  @impl true
  def init(elements) do
    initial_state = String.split(elements, ",", trim: true)
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:pop, _from, state) do
    [to_caller | new_state] = state
    {:reply, to_caller, new_state}
  end

  @impl true
  def handle_cast({:push, element}, state) do
    new_state = [element | state]
    {:noreply, new_state}
  end
end

我們將啟動、訊息傳遞和訊息迴圈的程序機制留給 GenServer 行為,只專注於堆疊實作。我們現在可以使用 GenServer API 透過建立一個程序並傳送訊息給它,來與服務互動。

# Start the server
{:ok, pid} = GenServer.start_link(Stack, "hello,world")

# This is the client
GenServer.call(pid, :pop)
#=> "hello"

GenServer.cast(pid, {:push, "elixir"})
#=> :ok

GenServer.call(pid, :pop)
#=> "elixir"

我們呼叫 start_link/2 來啟動我們的 Stack,傳遞包含伺服器實作的模組,以及其初始引數(元素清單,以逗號分隔)。GenServer 行為呼叫 init/1 回呼,以建立初始的 GenServer 狀態。從此點開始,GenServer 已取得控制權,因此我們透過在客戶端傳送兩種訊息來與之互動。呼叫訊息預期伺服器會回覆(因此是同步的),而傳送訊息則不會。

每次呼叫 GenServer.call/3 都會產生一則訊息,必須由 GenServer 中的 handle_call/3 回呼處理。cast/2 訊息必須由 handle_cast/2 處理。GenServer 支援 8 個回呼,但僅 init/1 是必要的。

使用 GenServer

當您 use GenServer 時,GenServer 模組會設定 @behaviour GenServer 並定義 child_spec/1 函式,因此您的模組可以用作監督樹中的子項。

客戶端/伺服器 API

雖然在上面的範例中,我們使用 GenServer.start_link/3 等函式直接啟動伺服器並與之通訊,但我們大多數時候不會直接呼叫 GenServer 函式。相反地,我們會將呼叫封裝在新的函式中,以表示伺服器的公開 API。這些薄封裝稱為客戶端 API

以下是我們 Stack 模組更好的實作

defmodule Stack do
  use GenServer

  # Client

  def start_link(default) when is_binary(default) do
    GenServer.start_link(__MODULE__, default)
  end

  def push(pid, element) do
    GenServer.cast(pid, {:push, element})
  end

  def pop(pid) do
    GenServer.call(pid, :pop)
  end

  # Server (callbacks)

  @impl true
  def init(elements) do
    initial_state = String.split(elements, ",", trim: true)
    {:ok, initial_state}
  end

  @impl true
  def handle_call(:pop, _from, state) do
    [to_caller | new_state] = state
    {:reply, to_caller, new_state}
  end

  @impl true
  def handle_cast({:push, element}, state) do
    new_state = [element | state]
    {:noreply, new_state}
  end
end

在實務上,通常會在同一個模組中同時包含伺服器和客戶端函式。如果伺服器和/或客戶端實作變得複雜,您可能希望將它們放在不同的模組中。

下圖摘要了客戶端和伺服器之間的互動。客戶端和伺服器都是程序,而通訊是透過訊息進行(實線)。伺服器 <-> 模組互動發生在 GenServer 程序呼叫您的程式碼時(虛線)

sequenceDiagram
    participant C as Client (Process)
    participant S as Server (Process)
    participant M as Module (Code)

    note right of C: Typically started by a supervisor
    C->>+S: GenServer.start_link(module, arg, options)
    S-->>+M: init(arg)
    M-->>-S: {:ok, state} | :ignore | {:error, reason}
    S->>-C: {:ok, pid} | :ignore | {:error, reason}

    note right of C: call is synchronous
    C->>+S: GenServer.call(pid, message)
    S-->>+M: handle_call(message, from, state)
    M-->>-S: {:reply, reply, state} | {:stop, reason, reply, state}
    S->>-C: reply

    note right of C: cast is asynchronous
    C-)S: GenServer.cast(pid, message)
    S-->>+M: handle_cast(message, state)
    M-->>-S: {:noreply, state} | {:stop, reason, state}

    note right of C: send is asynchronous
    C-)S: Kernel.send(pid, message)
    S-->>+M: handle_info(message, state)
    M-->>-S: {:noreply, state} | {:stop, reason, state}

如何監督

通常會在監督樹狀結構下啟動 GenServer。當我們呼叫 use GenServer 時,它會自動定義一個 child_spec/1 函數,讓我們可以在監督程式下直接啟動 Stack。若要在監督程式下啟動預設堆疊 ["hello", "world"],我們可以執行下列動作

children = [
  {Stack, "hello,world"}
]

Supervisor.start_link(children, strategy: :one_for_all)

請注意,指定模組 MyServer 會與指定元組 {MyServer, []} 相同。

use GenServer 也接受一個選項清單,用來設定子項規格,因此設定它在監督程式下執行的模式。產生的 child_spec/1 可以使用下列選項自訂

  • :id - 子項規格識別碼,預設為目前模組
  • :restart - 子項應重新啟動的時間,預設為 :permanent
  • :shutdown - 關閉子項的方式,立即關閉或給予時間讓它關閉

例如

use GenServer, restart: :transient, shutdown: 10_000

請參閱 Supervisor 模組中的「子項規格」區段,以取得更詳細的資訊。緊接在 use GenServer 之前的 @doc 註解會附加到產生的 child_spec/1 函數。

在停止 GenServer 時,例如從回呼傳回 {:stop, reason, new_state} 元組,監督程式會使用退出原因來判斷是否需要重新啟動 GenServer。請參閱 Supervisor 模組中的「退出原因和重新啟動」區段。

名稱註冊

start_link/3start/3 都支援 GenServer 透過 :name 選項在啟動時註冊名稱。註冊的名稱也會在終止時自動清除。支援的值為

  • 一個原子 - GenServer 使用 Process.register/2 註冊到本地(當前節點),並使用給定的名稱。

  • {:global, term} - GenServer 使用 :global 模組 中的函式,使用給定的術語進行全域註冊。

  • {:via, module, term} - GenServer 使用給定的機制和名稱進行註冊。 :via 選項預期一個模組,該模組會匯出 register_name/2unregister_name/1whereis_name/1send/2。一個這樣的範例是 :global 模組,它使用這些函式來維護名稱清單,以及與其關聯的 PID,這些名稱和 PID 可在 Elixir 節點網路中全域使用。Elixir 也隨附一個名為 Registry 的本地、分散式且可擴充的註冊表,用於在本地儲存動態產生的名稱。

例如,我們可以如下啟動並在本地註冊我們的 Stack 伺服器

# Start the server and register it locally with name MyStack
{:ok, _} = GenServer.start_link(Stack, "hello", name: MyStack)

# Now messages can be sent directly to MyStack
GenServer.call(MyStack, :pop)
#=> "hello"

伺服器啟動後,此模組中的其他函式(call/3cast/2,以及相關函式)也會接受一個原子,或任何 {:global, ...}{:via, ...} 元組。一般來說,支援下列格式

  • 一個 PID
  • 如果伺服器在本地註冊,則為一個原子
  • {atom, node} 如果伺服器在另一個節點上本地註冊
  • {:global, term} 如果伺服器已全域註冊
  • {:via, module, name} 如果伺服器透過替代註冊表註冊

如果想要在本地註冊動態名稱,請勿使用原子,因為原子絕不會被垃圾回收,因此動態產生的原子也不會被垃圾回收。對於這種情況,你可以使用 Registry 模組設定自己的本地註冊表。

接收「常規」訊息

GenServer 的目標是為開發人員抽象化「接收」迴圈,自動處理系統訊息、支援程式碼變更、同步呼叫等。因此,你絕不應該在 GenServer 回呼中呼叫你自己的「接收」,因為這樣會導致 GenServer 發生異常行為。

除了 call/3cast/2 提供的同步和非同步通訊之外,由 send/2Process.send_after/4 等函式傳送的「常規」訊息,可以在 handle_info/2 回呼中處理。

handle_info/2 可用於許多情況,例如處理 Process.monitor/1 傳送的監視器 DOWN 訊息。另一個 handle_info/2 的使用案例是執行定期工作,並藉助 Process.send_after/4

defmodule MyApp.Periodically do
  use GenServer

  def start_link(_) do
    GenServer.start_link(__MODULE__, %{})
  end

  @impl true
  def init(state) do
    # Schedule work to be performed on start
    schedule_work()

    {:ok, state}
  end

  @impl true
  def handle_info(:work, state) do
    # Do the desired work here
    # ...

    # Reschedule once more
    schedule_work()

    {:noreply, state}
  end

  defp schedule_work do
    # We schedule the work to happen in 2 hours (written in milliseconds).
    # Alternatively, one might write :timer.hours(2)
    Process.send_after(self(), :work, 2 * 60 * 60 * 1000)
  end
end

逾時

init/1 或任何 handle_* 回呼的傳回值可能包含以毫秒為單位的逾時值;如果沒有,則假設為 :infinity。逾時可偵測輸入訊息的平靜期。

timeout() 值會如下使用

  • 如果處理程序在傳回 timeout() 值時有任何訊息正在等待,則會忽略逾時,並照常處理等待訊息。這表示即使逾時為 0 毫秒,也無法保證執行(如果您想立即且無條件執行另一個動作,請改用 :continue 指令)。

  • 如果在指定的毫秒數到期前有任何訊息到達,則會清除逾時,並照常處理該訊息。

  • 否則,在指定的毫秒數到期且沒有訊息到達時,會以 :timeout 作為第一個引數呼叫 handle_info/2

何時(不)使用 GenServer

到目前為止,我們已了解 GenServer 可用作處理同步和非同步呼叫的受監督處理程序。它也可以處理系統訊息,例如週期性訊息和監控事件。GenServer 處理程序也可以命名。

GenServer 或一般處理程序必須用於模擬系統的執行時期特性。GenServer 絕不可用於程式碼組織目的。

在 Elixir 中,程式碼組織是由模組和函式完成的,不需要處理程序。例如,假設您正在實作計算器,並且決定將所有計算器運算放在 GenServer 之後

def add(a, b) do
  GenServer.call(__MODULE__, {:add, a, b})
end

def subtract(a, b) do
  GenServer.call(__MODULE__, {:subtract, a, b})
end

def handle_call({:add, a, b}, _from, state) do
  {:reply, a + b, state}
end

def handle_call({:subtract, a, b}, _from, state) do
  {:reply, a - b, state}
end

這是一個反模式,不僅因為它會混淆計算器邏輯,還因為您將計算器邏輯放在單一處理程序之後,這可能會成為系統中的瓶頸,特別是在呼叫數增加時。相反,只要直接定義函式即可

def add(a, b) do
  a + b
end

def subtract(a, b) do
  a - b
end

如果您不需要處理程序,那麼您就不需要處理程序。僅使用處理程序來模擬執行時期屬性,例如可變狀態、並行性和失敗,絕不使用處理程序來組織程式碼。

使用 :sys 模組進行除錯

GenServers,作為 特殊程序,可以使用 :sys 模組 進行除錯。透過各種掛鉤,此模組允許開發人員內省程序狀態並追蹤執行期間發生的系統事件,例如接收到的訊息、發送的回覆和狀態變更。

讓我們探討 :sys 模組 中用於除錯的基本函數

  • :sys.get_state/2 - 允許擷取程序狀態。對於 GenServer 程序,它將是回呼模組狀態,作為最後一個引數傳遞到回呼函數中。
  • :sys.get_status/2 - 允許擷取程序狀態。此狀態包括程序字典(如果程序正在執行或已暫停)、父項 PID、除錯器狀態,以及行為模組狀態(包括由 :sys.get_state/2 回傳的回呼模組狀態)。透過定義選用的 GenServer.format_status/2 回呼,可以變更此狀態的表示方式。
  • :sys.trace/3 - 將所有系統事件列印到 :stdio
  • :sys.statistics/3 - 管理程序統計資料的收集。
  • :sys.no_debug/2 - 關閉給定程序的所有除錯處理常式。完成除錯後,關閉除錯非常重要。過多的除錯處理常式或應關閉但未關閉的除錯處理常式,可能會嚴重損害系統效能。
  • :sys.suspend/2 - 允許暫停程序,使其僅回覆系統訊息,而不回覆其他訊息。暫停的程序可透過 :sys.resume/2 重新啟動。

讓我們看看如何使用這些函數來除錯我們先前定義的堆疊伺服器。

iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
*DBG* <0.122.0> got cast {push,1}
*DBG* <0.122.0> new state [1]
:ok

iex> :sys.get_state(pid)
[1]

iex> Stack.pop(pid)
*DBG* <0.122.0> got call pop from <0.80.0>
*DBG* <0.122.0> sent 1 to <0.80.0>, new state []
1

iex> :sys.statistics(pid, :get)
{:ok,
 [
   start_time: {{2016, 7, 16}, {12, 29, 41}},
   current_time: {{2016, 7, 16}, {12, 29, 50}},
   reductions: 117,
   messages_in: 2,
   messages_out: 0
 ]}

iex> :sys.no_debug(pid) # turn off all debug handlers
:ok

iex> :sys.get_status(pid)
{:status, #PID<0.122.0>, {:module, :gen_server},
 [
   [
     "$initial_call": {Stack, :init, 1},            # process dictionary
     "$ancestors": [#PID<0.80.0>, #PID<0.51.0>]
   ],
   :running,                                        # :running | :suspended
   #PID<0.80.0>,                                    # parent
   [],                                              # debugger state
   [
     header: 'Status for generic server <0.122.0>', # module status
     data: [
       {'Status', :running},
       {'Parent', #PID<0.80.0>},
       {'Logged events', []}
     ],
     data: [{'State', [1]}]
   ]
 ]}

深入了解

如果您想進一步了解 GenServers,Elixir 入門指南提供了類似教學課程的簡介。Erlang 中的文件和連結也可以提供額外的見解。

摘要

類型

start* 函數支援的偵錯選項

描述呼叫請求用戶端的元組。

GenServer 名稱

start* 函數的回傳值

start* 函數使用的選項值

start* 函數使用的選項

伺服器參考。

回呼

當載入不同版本的模組(熱程式碼交換)且狀態的術語結構應變更時,會呼叫此函數以變更 GenServer 的狀態。

在某些情況下會呼叫此函數以擷取 GenServer 狀態的格式化版本

呼叫此函數以處理同步 call/3 訊息。 call/3 會封鎖,直到收到回覆(除非呼叫逾時或節點已斷線)。

呼叫此函數以處理非同步 cast/2 訊息。

呼叫此函數以處理繼續指示。

呼叫此函數以處理所有其他訊息。

在伺服器啟動時呼叫此函數。 start_link/3start/3 會封鎖,直到它傳回。

在伺服器即將結束時呼叫此函數。它應執行任何必要的清理工作。

函數

在指定的節點上,將所有在本地註冊為 name 的伺服器進行廣播。

server 進行同步呼叫,並等待其回覆。

server 進行廣播請求,而不等待回應。

呼叫在指定的 nodes 上,所有在本地註冊為 name 的伺服器。

回覆用戶端。

啟動 GenServer 程序,不含連結(在監督樹之外)。

啟動 GenServer 程序,連結到目前的程序。

同步停止伺服器,並傳入 reason

傳回 GenServer 程序的 pid{name, node},否則傳回 nil

類型

@type debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t()}]

start* 函數支援的偵錯選項

@type from() :: {pid(), tag :: term()}

描述呼叫請求用戶端的元組。

pid 是呼叫者的 PID,而 tag 是用於識別呼叫的唯一術語。

@type name() :: atom() | {:global, term()} | {:via, module(), term()}

GenServer 名稱

@type on_start() ::
  {:ok, pid()} | :ignore | {:error, {:already_started, pid()} | term()}

start* 函數的回傳值

@type option() ::
  {:debug, debug()}
  | {:name, name()}
  | {:timeout, timeout()}
  | {:spawn_opt, [Process.spawn_opt()]}
  | {:hibernate_after, timeout()}

start* 函數使用的選項值

@type options() :: [option()]

start* 函數使用的選項

@type server() :: pid() | name() | {atom(), node()}

伺服器參考。

這可能是純粹的 PID 或代表已註冊名稱的值。請參閱此文件中的「名稱註冊」章節以取得更多資訊。

呼叫回函

連結到此呼叫回函

code_change(old_vsn, state, extra)

檢視原始碼 (選用)
@callback code_change(old_vsn, state :: term(), extra :: term()) ::
  {:ok, new_state :: term()} | {:error, reason :: term()}
when old_vsn: term() | {:down, term()}

當載入不同版本的模組(熱程式碼交換)且狀態的術語結構應變更時,會呼叫此函數以變更 GenServer 的狀態。

old_vsn 是升級時模組的先前版本(由 @vsn 屬性定義)。降級時,先前版本會封裝在 2 元組中,第一個元素為 :downstateGenServer 的目前狀態,而 extra 是變更狀態所需的任何額外資料。

傳回 {:ok, new_state} 會將狀態變更為 new_state,且程式碼變更成功。

傳回 {:error, reason} 會讓程式碼變更失敗,原因為 reason,且狀態仍為先前狀態。

如果 code_change/3 引發例外狀況,程式碼變更就會失敗,且迴圈會繼續執行其先前狀態。因此,此呼叫回函通常不包含副作用。

此呼叫回函為選用。

連結到此呼叫回函

format_status(reason, pdict_and_state)

檢視原始碼 (選用)
@callback format_status(reason, pdict_and_state :: list()) :: term()
when reason: :normal | :terminate

在某些情況下會呼叫此函數以擷取 GenServer 狀態的格式化版本

此回呼函式可協助控制 GenServer 狀態的外觀。例如,可用於傳回 GenServer 狀態的精簡表示,以避免列印大型狀態項目。

pdict_and_state 是兩個元素的清單 [pdict, state],其中 pdict{key, value} 叢集清單,表示 GenServer 的目前處理程序字典,而 stateGenServer 的目前狀態。

連結到此呼叫回函

handle_call(request, from, state)

檢視原始碼 (選用)
@callback handle_call(request :: term(), from(), state :: term()) ::
  {:reply, reply, new_state}
  | {:reply, reply, new_state,
     timeout() | :hibernate | {:continue, continue_arg :: term()}}
  | {:noreply, new_state}
  | {:noreply, new_state,
     timeout() | :hibernate | {:continue, continue_arg :: term()}}
  | {:stop, reason, reply, new_state}
  | {:stop, reason, new_state}
when reply: term(), new_state: term(), reason: term()

呼叫此函數以處理同步 call/3 訊息。 call/3 會封鎖,直到收到回覆(除非呼叫逾時或節點已斷線)。

requestcall/3 傳送的請求訊息,from 是包含呼叫者 PID 和唯一識別呼叫的項目的 2 元組,而 stateGenServer 的目前狀態。

傳回 {:reply, reply, new_state} 會將回應 reply 傳送給呼叫者,並以新狀態 new_state 繼續迴圈。

傳回 {:reply, reply, new_state, timeout} 類似於 {:reply, reply, new_state},但它也會設定逾時。如需更多資訊,請參閱模組文件中的「逾時」區段。

傳回 {:reply, reply, new_state, :hibernate} 類似於 {:reply, reply, new_state},但處理程序會暫停,並在訊息佇列中出現訊息後繼續迴圈。但是,如果訊息佇列中已有訊息,處理程序會立即繼續迴圈。暫停 GenServer 會導致垃圾回收,並留下連續的堆,將處理程序使用的記憶體降至最低。

不應積極暫停,因為可能會花費太多時間進行垃圾回收,這會延遲處理接收訊息。通常只應在您不預期立即收到新訊息,且已顯示將處理程序的記憶體降至最低是有益處時使用。

傳回 {:reply, reply, new_state, {:continue, continue_arg}} 類似於 {:reply, reply, new_state},但 handle_continue/2 會在 continue_arg 作為第一個引數,而 state 作為第二個引數後立即呼叫。

傳回 {:noreply, new_state} 不會對呼叫者傳送回應,並以新狀態 new_state 繼續迴圈。必須使用 reply/2 傳送回應。

不使用傳回值來回覆有三個主要的用例

  • 在從回呼傳回之前回覆,因為在呼叫慢速函數之前已知回應。
  • 在從回呼傳回之後回覆,因為回應尚未可用。
  • 從另一個程序(例如任務)回覆。

從另一個程序回覆時,如果另一個程序在未回覆的情況下結束,GenServer 應結束,因為呼叫者會封鎖等待回覆。

傳回 {:noreply, new_state, timeout | :hibernate | {:continue, continue_arg}} 類似於 {:noreply, new_state},但會發生逾時、休眠或繼續,就像 :reply 元組一樣。

傳回 {:stop, reason, reply, new_state} 會停止迴圈,並以原因 reason 和狀態 new_state 呼叫 terminate/2。然後,reply 會作為呼叫的回應傳送,並且程序會以原因 reason 結束。

傳回 {:stop, reason, new_state} 類似於 {:stop, reason, reply, new_state},但不會傳送回覆。

此回呼是選用的。如果未實作一個,則如果對其執行呼叫,伺服器將會失敗。

連結到此呼叫回函

handle_cast(request, state)

檢視原始碼 (選用)
@callback handle_cast(request :: term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state,
     timeout() | :hibernate | {:continue, continue_arg :: term()}}
  | {:stop, reason :: term(), new_state}
when new_state: term()

呼叫此函數以處理非同步 cast/2 訊息。

requestcast/2 傳送的請求訊息,而 stateGenServer 的目前狀態。

傳回 {:noreply, new_state} 會以新狀態 new_state 繼續迴圈。

傳回 {:noreply, new_state, timeout} 類似於 {:noreply, new_state},但它也會設定逾時。請參閱模組文件中的「逾時」區段以取得更多資訊。

傳回 {:noreply, new_state, :hibernate} 類似於 {:noreply, new_state},但程序會在繼續迴圈之前休眠。請參閱 handle_call/3 以取得更多資訊。

傳回 {:noreply, new_state, {:continue, continue_arg}} 類似於 {:noreply, new_state},但 handle_continue/2 會在 continue_arg 作為第一個引數,而 state 作為第二個引數後立即呼叫。

傳回 {:stop, reason, new_state} 會停止迴圈,並呼叫 terminate/2,其中包含原因 reason 和狀態 new_state。程序會以原因 reason 退出。

這個回呼是選用的。如果沒有實作,伺服器會在對其執行轉型時失敗。

連結到此呼叫回函

handle_continue(continue_arg, state)

檢視原始碼 (選用)
@callback handle_continue(continue_arg, state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state, timeout() | :hibernate | {:continue, continue_arg}}
  | {:stop, reason :: term(), new_state}
when new_state: term(), continue_arg: term()

呼叫此函數以處理繼續指示。

這對於在初始化後執行工作,或將回呼中的工作分成多個步驟,並在過程中更新程序狀態很有用。

傳回值與 handle_cast/2 相同。

這個回呼是選用的。如果沒有實作,伺服器會在使用 continue 指令時失敗。

連結到此呼叫回函

handle_info(msg, state)

檢視原始碼 (選用)
@callback handle_info(msg :: :timeout | term(), state :: term()) ::
  {:noreply, new_state}
  | {:noreply, new_state,
     timeout() | :hibernate | {:continue, continue_arg :: term()}}
  | {:stop, reason :: term(), new_state}
when new_state: term()

呼叫此函數以處理所有其他訊息。

msg 是訊息,而 stateGenServer 的目前狀態。當逾時發生時,訊息為 :timeout

傳回值與 handle_cast/2 相同。

這個回呼是選用的。如果沒有實作,接收到的訊息將會被記錄。

@callback init(init_arg :: term()) ::
  {:ok, state}
  | {:ok, state, timeout() | :hibernate | {:continue, continue_arg :: term()}}
  | :ignore
  | {:stop, reason :: any()}
when state: any()

在伺服器啟動時呼叫此函數。 start_link/3start/3 會封鎖,直到它傳回。

init_arg 是傳遞給 start_link/3 的引數項目(第二個引數)。

傳回 {:ok, state} 會導致 start_link/3 傳回 {:ok, pid},並讓程序進入其迴圈。

傳回 {:ok, state, timeout} 類似於 {:ok, state},但它也會設定逾時。有關更多資訊,請參閱模組文件中的「逾時」區段。

傳回 {:ok, state, :hibernate} 類似於 {:ok, state},但程序在進入迴圈之前會休眠。有關休眠的更多資訊,請參閱 handle_call/3

傳回 {:ok, state, {:continue, continue_arg}} 類似於 {:ok, state},但立即在進入迴圈後,handle_continue/2 回呼會被呼叫,其中 continue_arg 為第一個引數,而 state 為第二個引數。

傳回 :ignore 將導致 start_link/3 傳回 :ignore,且程序會正常結束,而不會進入迴圈或呼叫 terminate/2。如果在監督樹的某個部分使用時,父監督程式不會無法啟動,也不會立即嘗試重新啟動 GenServer。監督樹的其餘部分將會啟動,因此 GenServer 不應為其他程序所需要。它可以在稍後透過 Supervisor.restart_child/2 啟動,因為子項規格已儲存在父監督程式中。這樣做主要有以下幾個用例:

傳回 {:stop, reason} 將導致 start_link/3 傳回 {:error, reason},且程序以原因 reason 結束,而不會進入迴圈或呼叫 terminate/2

連結到此呼叫回函

terminate(reason, state)

檢視原始碼 (選用)
@callback terminate(reason, state :: term()) :: term()
when reason: :normal | :shutdown | {:shutdown, term()} | term()

在伺服器即將結束時呼叫此函數。它應執行任何必要的清理工作。

reason 是結束原因,stateGenServer 的目前狀態。傳回值會被忽略。

terminate/2 對於需要存取 GenServer 狀態的清理很有用。不過,無法保證GenServer 結束時會呼叫 terminate/2。因此,重要的清理工作應使用程序連結和/或監視器來完成。監視程序將收到傳遞給 terminate/2 的相同結束 reason

terminate/2 會在以下情況下被呼叫:

  • GenServer 攔截退出(使用 Process.flag/2 父程序(呼叫 start_link/1 的程序)傳送退出訊號時

  • 當回呼(除了 init/1)執行下列其中一項操作時:

    • 傳回 :stop 元組

    • 引發(透過 raise/2)或退出(透過 exit/1

    • 傳回無效值

如果 GenServer 是監督樹的一部分,當樹關閉時,它將從其父程序(其監督者)接收退出訊號。退出訊號基於子項規格中的關閉策略,此值可以是:

  • :brutal_killGenServer 會被終止,因此不會呼叫 terminate/2

  • 逾時值,監督者會傳送退出訊號 :shutdown,而 GenServer 會有逾時持續時間來終止。如果在逾時持續時間後,程序仍然執行,它將立即被終止。

如需更深入的說明,請參閱 Supervisor 模組中的「關閉值 (:shutdown)」區段。

如果 GenServer 從任何程序接收退出訊號(不是 :normal),而它並未攔截退出,它將以相同原因突然退出,因此不會呼叫 terminate/2。請注意,程序預設不會攔截退出,當連結的程序退出或其節點中斷時,會傳送退出訊號。

僅在 GenServer 完成處理在退出訊號之前傳送到其郵件匣的所有訊息後,才會呼叫 terminate/2。如果它在完成處理這些訊息之前收到 :kill 訊號,則不會呼叫 terminate/2。如果呼叫 terminate/2,退出訊號後收到的任何訊息仍會在郵件匣中。

GenServer 控制 port(例如,:gen_tcp.socket)或 File.io_device/0 時,不需要清除,因為這些會在收到 GenServer 的退出訊號時關閉,且不需要在 terminate/2 中手動關閉。

如果 reason 不是 :normal:shutdown{:shutdown, term},則會記錄錯誤。

此呼叫回函為選用。

函數

連結到此函數

abcast(nodes \\ [node() | Node.list()], name, request)

檢視原始碼
@spec abcast([node()], name :: atom(), term()) :: :abcast

在指定的節點上,將所有在本地註冊為 name 的伺服器進行廣播。

此函數會立即傳回,並忽略不存在的節點或伺服器名稱不存在的節點。

請參閱 multi_call/4 以取得更多資訊。

連結到此函數

call(server, request, timeout \\ 5000)

檢視原始碼
@spec call(server(), term(), timeout()) :: term()

server 進行同步呼叫,並等待其回覆。

用戶端會將指定的 request 傳送至伺服器,並等到收到回覆或發生逾時。 handle_call/3 會在伺服器上呼叫以處理請求。

server 可以是此模組文件中的「名稱註冊」區段中所述的任何值。

逾時

timeout 是大於 0 的整數,指定要等待回覆的毫秒數,或原子 :infinity 表示無限期等待。預設值為 5000。如果在指定時間內未收到回覆,函數呼叫會失敗,且呼叫者會退出。如果呼叫者捕捉到失敗並繼續執行,而伺服器只是回覆延遲,則回覆可能會在稍後任何時間傳送到呼叫者的訊息佇列中。呼叫者在此情況下必須做好準備,並捨棄任何此類垃圾訊息,這些訊息是包含參考作為第一個元素的二元組。

@spec cast(server(), term()) :: :ok

server 進行廣播請求,而不等待回應。

此函數始終傳回 :ok,無論目的地 server(或節點)是否存在。因此,無法得知目的地 server 是否已成功處理請求。

server 可以是此模組文件中的「名稱註冊」區段中所述的任何值。

連結到此函數

multi_call(nodes \\ [node() | Node.list()], name, request, timeout \\ :infinity)

檢視原始碼
@spec multi_call([node()], name :: atom(), term(), timeout()) ::
  {replies :: [{node(), term()}], bad_nodes :: [node()]}

呼叫在指定的 nodes 上,所有在本地註冊為 name 的伺服器。

首先,會將 request 傳送至 nodes 中的每個節點;然後,呼叫者會等待回覆。此函數傳回二元組 {replies, bad_nodes},其中

  • replies - 是 {node, reply} 組合的清單,其中 node 是回覆的節點,而 reply 是其回覆
  • bad_nodes - 是不存在的節點清單,或伺服器具有指定的 name 但不存在或未回覆的節點清單

nodes 是傳送請求的節點名稱清單。預設值是所有已知節點的清單(包括此節點)。

範例

假設 GenServer 模組文件所述的 Stack GenServer 在 :"foo@my-machine":"bar@my-machine" 節點中註冊為 Stack

GenServer.multi_call(Stack, :pop)
#=> {[{:"foo@my-machine", :hello}, {:"bar@my-machine", :world}], []}
@spec reply(from(), term()) :: :ok

回覆用戶端。

此函數可用於明確傳送回覆給呼叫 call/3multi_call/4 的用戶端,當無法在 handle_call/3 的傳回值中指定回覆時。

client 必須是 handle_call/3 回呼所接受的 from 參數(第二個參數)。reply 是任意術語,會作為呼叫的回傳值傳回給 client。

請注意,reply/2 可以從任何程序呼叫,不限於最初收到呼叫的 GenServer(只要該 GenServer 以某種方式傳達 from 參數即可)。

此函式總是傳回 :ok

範例

def handle_call(:reply_in_one_second, from, state) do
  Process.send_after(self(), {:reply, from}, 1_000)
  {:noreply, state}
end

def handle_info({:reply, from}, state) do
  GenServer.reply(from, :one_second_has_passed)
  {:noreply, state}
end
連結到此函數

start(module, init_arg, options \\ [])

檢視原始碼
@spec start(module(), any(), options()) :: on_start()

啟動 GenServer 程序,不含連結(在監督樹之外)。

請參閱 start_link/3 以取得更多資訊。

連結到此函數

start_link(module, init_arg, options \\ [])

檢視原始碼
@spec start_link(module(), any(), options()) :: on_start()

啟動 GenServer 程序,連結到目前的程序。

這通常用於啟動 GenServer 作為監督樹的一部分。

伺服器啟動後,給定 moduleinit/1 函式會呼叫,並以 init_arg 作為參數來初始化伺服器。為了確保同步啟動程序,此函式在 init/1 傳回之前不會傳回。

請注意,使用 start_link/3 啟動的 GenServer 會連結到父程序,如果父程序發生崩潰,它也會結束。如果 GenServer 設定為在 init/1 回呼中攔截結束,它也會因為 :normal 原因而結束。

選項

  • :name - 用於名稱註冊,如 GenServer 文件中的「名稱註冊」區段所述

  • :timeout - 如果存在,伺服器允許花費指定的毫秒數進行初始化,否則它將終止,而啟動函式將傳回 {:error, :timeout}

  • :debug - 如果存在,會呼叫 :sys 模組 中對應的函式

  • :spawn_opt - 如果存在,其值會作為選項傳遞給基礎程序,如 Process.spawn/4

  • :hibernate_after - 如果存在,GenServer 程序會等待任何訊息指定的毫秒數,如果未收到訊息,程序會自動進入休眠狀態(透過呼叫 :proc_lib.hibernate/3)。

傳回值

如果伺服器已成功建立並初始化,此函式會傳回 {:ok, pid},其中 pid 是伺服器的 PID。如果已存在具有指定伺服器名稱的程序,此函式會傳回 {:error, {:already_started, pid}},其中包含該程序的 PID。

如果 init/1 回呼以 reason 失敗,此函式會傳回 {:error, reason}。否則,如果傳回 {:stop, reason}:ignore,程序會終止,此函式會分別傳回 {:error, reason}:ignore

連結到此函數

stop(server, reason \\ :normal, timeout \\ :infinity)

檢視原始碼
@spec stop(server(), reason :: term(), timeout()) :: :ok

同步停止伺服器,並傳入 reason

在退出之前,會呼叫指定 serverterminate/2 回呼。如果伺服器以指定原因終止,此函式會傳回 :ok;如果以其他原因終止,呼叫會退出。

此函式會保留 OTP 語意,以進行錯誤回報。如果原因為 :normal:shutdown{:shutdown, _} 以外的其他原因,將會記錄錯誤回報。

@spec whereis(server()) :: pid() | {atom(), node()} | nil

傳回 GenServer 程序的 pid{name, node},否則傳回 nil

更精確地說,如果無法傳回 pid{name, node},將會傳回 nil。請注意,無法保證傳回的 pid{name, node} 是存在的,因為程序在查詢後可能會立即終止。

範例

例如,要查詢伺服器程序、監控它並傳送 cast 給它

process = GenServer.whereis(server)
monitor = Process.monitor(process)
GenServer.cast(process, :hello)