檢視原始碼 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/3
和 start/3
都支援 GenServer
透過 :name
選項在啟動時註冊名稱。註冊的名稱也會在終止時自動清除。支援的值為
一個原子 - GenServer 使用
Process.register/2
註冊到本地(當前節點),並使用給定的名稱。{:global, term}
- GenServer 使用:global
模組 中的函式,使用給定的術語進行全域註冊。{:via, module, term}
- GenServer 使用給定的機制和名稱進行註冊。:via
選項預期一個模組,該模組會匯出register_name/2
、unregister_name/1
、whereis_name/1
和send/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/3
、cast/2
,以及相關函式)也會接受一個原子,或任何 {:global, ...}
或 {:via, ...}
元組。一般來說,支援下列格式
- 一個 PID
- 如果伺服器在本地註冊,則為一個原子
{atom, node}
如果伺服器在另一個節點上本地註冊{:global, term}
如果伺服器已全域註冊{:via, module, name}
如果伺服器透過替代註冊表註冊
如果想要在本地註冊動態名稱,請勿使用原子,因為原子絕不會被垃圾回收,因此動態產生的原子也不會被垃圾回收。對於這種情況,你可以使用 Registry
模組設定自己的本地註冊表。
接收「常規」訊息
GenServer
的目標是為開發人員抽象化「接收」迴圈,自動處理系統訊息、支援程式碼變更、同步呼叫等。因此,你絕不應該在 GenServer 回呼中呼叫你自己的「接收」,因為這樣會導致 GenServer 發生異常行為。
除了 call/3
和 cast/2
提供的同步和非同步通訊之外,由 send/2
、Process.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
狀態的格式化版本
呼叫此函數以處理非同步 cast/2
訊息。
呼叫此函數以處理繼續指示。
呼叫此函數以處理所有其他訊息。
在伺服器啟動時呼叫此函數。 start_link/3
或 start/3
會封鎖,直到它傳回。
在伺服器即將結束時呼叫此函數。它應執行任何必要的清理工作。
類型
@type debug() :: [:trace | :log | :statistics | {:log_to_file, Path.t()}]
由 start*
函數支援的偵錯選項
描述呼叫請求用戶端的元組。
pid
是呼叫者的 PID,而 tag
是用於識別呼叫的唯一術語。
GenServer 名稱
start*
函數的回傳值
@type option() :: {:debug, debug()} | {:name, name()} | {:timeout, timeout()} | {:spawn_opt, [Process.spawn_opt()]} | {:hibernate_after, timeout()}
start*
函數使用的選項值
@type options() :: [option()]
start*
函數使用的選項
伺服器參考。
這可能是純粹的 PID 或代表已註冊名稱的值。請參閱此文件中的「名稱註冊」章節以取得更多資訊。
呼叫回函
@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 元組中,第一個元素為 :down
。 state
是 GenServer
的目前狀態,而 extra
是變更狀態所需的任何額外資料。
傳回 {:ok, new_state}
會將狀態變更為 new_state
,且程式碼變更成功。
傳回 {:error, reason}
會讓程式碼變更失敗,原因為 reason
,且狀態仍為先前狀態。
如果 code_change/3
引發例外狀況,程式碼變更就會失敗,且迴圈會繼續執行其先前狀態。因此,此呼叫回函通常不包含副作用。
此呼叫回函為選用。
@callback format_status(reason, pdict_and_state :: list()) :: term() when reason: :normal | :terminate
在某些情況下會呼叫此函數以擷取 GenServer
狀態的格式化版本
會呼叫
:sys.get_status/1
或:sys.get_status/2
之一,以取得GenServer
的狀態;在這種情況下,reason
為:normal
如果
GenServer
異常終止並記錄錯誤,在這種情況下,reason
為:terminate
此回呼函式可協助控制 GenServer
狀態的外觀。例如,可用於傳回 GenServer
狀態的精簡表示,以避免列印大型狀態項目。
pdict_and_state
是兩個元素的清單 [pdict, state]
,其中 pdict
是 {key, value}
叢集清單,表示 GenServer
的目前處理程序字典,而 state
是 GenServer
的目前狀態。
@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
會封鎖,直到收到回覆(除非呼叫逾時或節點已斷線)。
request
是 call/3
傳送的請求訊息,from
是包含呼叫者 PID 和唯一識別呼叫的項目的 2 元組,而 state
是 GenServer
的目前狀態。
傳回 {: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}
,但不會傳送回覆。
此回呼是選用的。如果未實作一個,則如果對其執行呼叫,伺服器將會失敗。
@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
訊息。
request
是 cast/2
傳送的請求訊息,而 state
是 GenServer
的目前狀態。
傳回 {: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
退出。
這個回呼是選用的。如果沒有實作,伺服器會在對其執行轉型時失敗。
@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 指令時失敗。
@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
是訊息,而 state
是 GenServer
的目前狀態。當逾時發生時,訊息為 :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/3
或 start/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
啟動,因為子項規格已儲存在父監督程式中。這樣做主要有以下幾個用例:
GenServer
已由組態停用,但稍後可能會啟用。- 發生錯誤,且將由
Supervisor
以外的機制處理。這種方法可能涉及在延遲後呼叫Supervisor.restart_child/2
以嘗試重新啟動。
傳回 {:stop, reason}
將導致 start_link/3
傳回 {:error, reason}
,且程序以原因 reason
結束,而不會進入迴圈或呼叫 terminate/2
。
@callback terminate(reason, state :: term()) :: term() when reason: :normal | :shutdown | {:shutdown, term()} | term()
在伺服器即將結束時呼叫此函數。它應執行任何必要的清理工作。
reason
是結束原因,state
是 GenServer
的目前狀態。傳回值會被忽略。
terminate/2
對於需要存取 GenServer
狀態的清理很有用。不過,無法保證在 GenServer
結束時會呼叫 terminate/2
。因此,重要的清理工作應使用程序連結和/或監視器來完成。監視程序將收到傳遞給 terminate/2
的相同結束 reason
。
terminate/2
會在以下情況下被呼叫:
當
GenServer
攔截退出(使用Process.flag/2
)且 父程序(呼叫start_link/1
的程序)傳送退出訊號時當回呼(除了
init/1
)執行下列其中一項操作時:
如果 GenServer
是監督樹的一部分,當樹關閉時,它將從其父程序(其監督者)接收退出訊號。退出訊號基於子項規格中的關閉策略,此值可以是:
:brutal_kill
:GenServer
會被終止,因此不會呼叫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}
,則會記錄錯誤。
此呼叫回函為選用。
函數
在指定的節點上,將所有在本地註冊為 name
的伺服器進行廣播。
此函數會立即傳回,並忽略不存在的節點或伺服器名稱不存在的節點。
請參閱 multi_call/4
以取得更多資訊。
對 server
進行同步呼叫,並等待其回覆。
用戶端會將指定的 request
傳送至伺服器,並等到收到回覆或發生逾時。 handle_call/3
會在伺服器上呼叫以處理請求。
server
可以是此模組文件中的「名稱註冊」區段中所述的任何值。
逾時
timeout
是大於 0 的整數,指定要等待回覆的毫秒數,或原子 :infinity
表示無限期等待。預設值為 5000
。如果在指定時間內未收到回覆,函數呼叫會失敗,且呼叫者會退出。如果呼叫者捕捉到失敗並繼續執行,而伺服器只是回覆延遲,則回覆可能會在稍後任何時間傳送到呼叫者的訊息佇列中。呼叫者在此情況下必須做好準備,並捨棄任何此類垃圾訊息,這些訊息是包含參考作為第一個元素的二元組。
對 server
進行廣播請求,而不等待回應。
此函數始終傳回 :ok
,無論目的地 server
(或節點)是否存在。因此,無法得知目的地 server
是否已成功處理請求。
server
可以是此模組文件中的「名稱註冊」區段中所述的任何值。
@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}], []}
回覆用戶端。
此函數可用於明確傳送回覆給呼叫 call/3
或 multi_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
啟動 GenServer
程序,不含連結(在監督樹之外)。
請參閱 start_link/3
以取得更多資訊。
啟動 GenServer
程序,連結到目前的程序。
這通常用於啟動 GenServer
作為監督樹的一部分。
伺服器啟動後,給定 module
的 init/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
。
同步停止伺服器,並傳入 reason
。
在退出之前,會呼叫指定 server
的 terminate/2
回呼。如果伺服器以指定原因終止,此函式會傳回 :ok
;如果以其他原因終止,呼叫會退出。
此函式會保留 OTP 語意,以進行錯誤回報。如果原因為 :normal
、:shutdown
或 {:shutdown, _}
以外的其他原因,將會記錄錯誤回報。
傳回 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)