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。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。