Elixir語言中 GenServer
相關用法介紹如下。
用於實現client-server 關係的服務器的行為模塊。
GenServer 是與任何其他 Elixir 進程一樣的進程,它可用於保持狀態、異步執行代碼等。使用使用此模塊實現的通用服務器進程 (GenServer) 的優點是它將具有一組標準的接口函數,並包括用於跟蹤和錯誤報告的函數。它也將適合監督樹。
示例
GenServer 行為抽象了常見的client-server 交互。開發人員隻需要實現他們感興趣的回調和函數。
讓我們從一個代碼示例開始,然後探索可用的回調。想象一下,我們想要一個像堆棧一樣工作的GenServer,允許我們推送和彈出元素:
defmodule Stack do
use GenServer
# Callbacks
@impl true
def init(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
# Start the server
{:ok, pid} = GenServer.start_link(Stack, [:hello])
# This is the client
GenServer.call(pid, :pop)
#=> :hello
GenServer.cast(pid, {:push, :world})
#=> :ok
GenServer.call(pid, :pop)
#=> :world
我們通過調用
開始我們的 start_link/2
Stack
,將模塊與服務器實現及其初始參數(表示包含元素 :hello
的堆棧的列表)一起傳遞。我們主要可以通過發送兩種類型的消息與服務器進行交互。調用消息期望來自服務器的回複(因此是同步的),而轉換消息則不需要。
每次執行
時,客戶端都會發送一條消息,該消息必須由 GenServer 中的 GenServer.call/3
回調處理。 handle_call/3
消息必須由 cast/2
處理。當您使用 handle_cast/2
時,有 8 個可能的回調被實現。唯一需要的回調是 GenServer
。init/1
客戶端/服務器 API
雖然在上麵的例子中我們已經使用
和朋友直接啟動並與服務器通信,但大多數時候我們並沒有直接調用GenServer.start_link/3
函數。相反,我們將調用包裝在代表服務器公共 API 的新函數中。GenServer
這是我們的 Stack 模塊的一個更好的實現:
defmodule Stack do
use GenServer
# Client
def start_link(default) when is_list(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(stack) do
{:ok, stack}
end
@impl true
def handle_call(:pop, _from, [head | tail]) do
{:reply, head, tail}
end
@impl true
def handle_cast({:push, element}, state) do
{:noreply, [element | state]}
end
end
實際上,在同一個模塊中同時具有服務器和客戶端函數是很常見的。如果服務器和/或客戶端實現變得越來越複雜,您可能希望將它們放在不同的模塊中。
如何監督
最常在監督樹下啟動。當我們調用 GenServer
use GenServer
時,它會自動定義一個 child_spec/1
函數,允許我們直接在主管下啟動 Stack
。要在主管下啟動 [:hello]
的默認堆棧,可以執行以下操作:
children = [
{Stack, [:hello]}
]
Supervisor.start_link(children, strategy: :one_for_all)
請注意,您也可以將其簡單地啟動為 Stack
,這與 {Stack, []}
相同:
children = [
Stack # The same as {Stack, []}
]
Supervisor.start_link(children, strategy: :one_for_all)
在這兩種情況下,總是調用Stack.start_link/1
。
use GenServer
還接受一個選項列表,用於配置子規範以及它如何在主管下運行。生成的child_spec/1
可以使用以下選項進行自定義:
:id
- 子規範標識符,默認為當前模塊:restart
- 當孩子應該重新啟動時,默認為:permanent
:shutdown
- 如何立即關閉孩子,或者讓孩子有時間關閉
例如:
use GenServer, restart: :transient, shutdown: 10_000
有關更多詳細信息,請參閱
模塊中的"Child specification" 部分。緊接在Supervisor
use GenServer
之前的@doc
注釋將附加到生成的child_spec/1
函數。
名稱注冊
和 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
和Kernel.send.一個這樣的例子是: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
{atom, node}
如果服務器在另一個節點本地注冊{:global, term}
如果服務器是全局注冊的{:via, module, name}
如果服務器是通過備用注冊表注冊的
如果有興趣在本地注冊動態名稱,請不要使用原子,因為原子永遠不是garbage-collected,因此動態生成的原子不會是garbage-collected。對於這種情況,您可以使用
模塊設置您自己的本地注冊表。Registry
接收"regular" 消息
的目標是為開發人員抽象"receive" 循環,自動處理係統消息,支持代碼更改,同步調用等。因此,您永遠不應在GenServer 回調中調用您自己的"receive",因為這樣做會導致GenServer 行為不端。GenServer
除了
和call/3
提供的同步和異步通信之外,cast/2
、Kernel.send/2
等函數發送的"regular"消息也可以在Process.send_after/4
回調中處理。handle_info/2
可以在很多情況下使用,例如處理 handle_info/2
發送的監視器 DOWN 消息。 Process.monitor/1
的另一個用例是在 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
指令)。 -
如果任何消息在指定的毫秒數過去之前到達,則超時被清除並照常處理該消息。
-
否則,當指定的毫秒數過去後沒有消息到達時,將調用
handle_info/2
,並將:timeout
作為第一個參數。
何時(不)使用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
:sys.get_status/2
: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 入門指南提供了tutorial-like 介紹。 Erlang 中的文檔和鏈接也可以提供額外的見解。
相關用法
- Elixir GenServer.reply用法及代碼示例
- Elixir GenServer.multi_call用法及代碼示例
- Elixir GenServer.whereis用法及代碼示例
- Elixir StringIO.flush用法及代碼示例
- Elixir Calendar.ISO.date_to_string用法及代碼示例
- Elixir Enum.unzip用法及代碼示例
- Elixir Date.add用法及代碼示例
- Elixir Keyword.get用法及代碼示例
- Elixir Stream用法及代碼示例
- Elixir Registry.count_match用法及代碼示例
- Elixir List.keyfind!用法及代碼示例
- Elixir URI.decode用法及代碼示例
- Elixir Integer.pow用法及代碼示例
- Elixir NaiveDateTime用法及代碼示例
- Elixir Enum.min_max用法及代碼示例
- Elixir Path.basename用法及代碼示例
- Elixir Code.prepend_path用法及代碼示例
- Elixir Calendar.ISO.time_to_string用法及代碼示例
- Elixir Bitwise.~~~expr用法及代碼示例
- Elixir Kernel.SpecialForms.case用法及代碼示例
- Elixir String.contains?用法及代碼示例
- Elixir Map.keys用法及代碼示例
- Elixir Version用法及代碼示例
- Elixir Kernel.round用法及代碼示例
- Elixir Kernel.left / right用法及代碼示例
注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 GenServer。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。