当前位置: 首页>>代码示例 >>用法及示例精选 >>正文


Elixir GenServer用法及代码示例


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.call/3 时,客户端都会发送一条消息,该消息必须由 GenServer 中的 handle_call/3 回调处理。 cast/2 消息必须由 handle_cast/2 处理。当您使用 GenServer 时,有 8 个可能的回调被实现。唯一需要的回调是 init/1

客户端/服务器 API

虽然在上面的例子中我们已经使用 GenServer.start_link/3 和朋友直接启动并与服务器通信,但大多数时候我们并没有直接调用 GenServer 函数。相反,我们将调用包装在代表服务器公共 API 的新函数中。

这是我们的 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

有关更多详细信息,请参阅 Supervisor 模块中的"Child specification" 部分。紧接在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/1Kernel.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" 消息

GenServer 的目标是为开发人员抽象"receive" 循环,自动处理系统消息,支持代码更改,同步调用等。因此,您永远不应在GenServer 回调中调用您自己的"receive",因为这样做会导致GenServer 行为不端。

除了 call/3 cast/2 提供的同步和异步通信之外, Kernel.send/2 Process.send_after/4 等函数发送的"regular"消息也可以在 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 指令)。

  • 如果任何消息在指定的毫秒数过去之前到达,则超时被清除并照常处理该消息。

  • 否则,当指定的毫秒数过去后没有消息到达时,将调用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 - 允许检索进程的状态。在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 入门指南提供了tutorial-like 介绍。 Erlang 中的文档和链接也可以提供额外的见解。

相关用法


注:本文由纯净天空筛选整理自elixir-lang.org大神的英文原创作品 GenServer。非经特殊声明,原始代码版权归原作者所有,本译文未经允许或授权,请勿转载或复制。