查看源代码 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 的初始参数转换为 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 将控制权,所以我们通过在客户端发送两种类型的消息来与它交互。**call** 消息期望来自服务器的回复(因此是同步的),而 **cast** 消息则不期望。

每次调用 GenServer.call/3 都会产生一条消息,该消息必须由 GenServer 中的 handle_call/3 回调处理。一个 cast/2 消息必须由 handle_cast/2 处理。GenServer 支持 8 个回调,但只有 init/1 是必需的。

use 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 模块中的“子节点规范”部分。@doc 注解紧接在 use GenServer 之前,将附加到生成的 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 模块,它使用这些函数来维护可供 Elixir 节点网络全局访问的进程名称列表及其关联的 PID。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 指令)。

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

  • 否则,当指定的毫秒数过去而没有消息到达时,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 模块进行调试

GenServer 作为 特殊进程,可以使用 :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 属性定义)。在降级时,先前版本包装在具有第一个元素 :down 的 2 元组中。 stateGenServer 的当前状态,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 将阻塞,直到收到回复(除非调用超时或节点断开连接)。

request 是由 call/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)

View Source (可选)
@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 发送的请求消息,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} 会停止循环,并使用原因 reason 和状态 new_state 调用 terminate/2。进程使用原因 reason 退出。

此回调函数是可选的。如果没有实现,如果执行广播,服务器将会失败。

链接到此回调

handle_continue(continue_arg, state)

View Source (可选)
@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 相同。

此回调函数是可选的。如果没有实现,如果使用继续指令,服务器将会失败。

链接到此回调

handle_info(msg, state)

View Source (可选)
@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 在以后启动,因为子级规范保存在父级监督者中。这主要用于以下用例:

  • GenServer 被配置禁用,但可能在以后被启用。
  • 发生了错误,并且将由与 Supervisor 不同的机制进行处理。这种方法可能包括在延迟之后调用 Supervisor.restart_child/2 以尝试重新启动。

返回 {:stop, reason} 会导致 start_link/3 返回 {:error, reason},并且进程使用原因 reason 退出,不会进入循环或调用 terminate/2

链接到此回调

terminate(reason, state)

View Source (可选)
@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。请注意,进程默认情况下不会捕获退出,并且当链接的进程退出或其节点断开连接时会发送退出信号。

terminate/2 只在 GenServer 完成处理退出信号之前到达其邮箱中的所有消息之后调用。如果它在完成处理这些消息之前接收到 :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 是一个大于零的整数,它指定等待回复的毫秒数,或者原子 :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 是一个节点名称列表,请求将发送到这些节点。默认值是所有已知节点的列表(包括此节点)。

示例

假设在 :"foo@my-machine":"bar@my-machine" 节点中,Stack GenServer 在 GenServer 模块文档中提及的注册为 Stack

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

回复客户端。

此函数可用于在无法在 handle_call/3 的返回值中指定回复时,显式地向调用了 call/3multi_call/4 的客户端发送回复。

client 必须是 handle_call/3 回调接受的 from 参数(第二个参数)。reply 是一个任意项,它将作为调用的返回值返回给客户端。

请注意,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 作为监督树的一部分启动。

服务器启动后,将使用 init_arg 作为参数调用给定 moduleinit/1 函数来初始化服务器。为了确保同步启动过程,此函数不会返回,直到 init/1 返回为止。

请注意,使用 start_link/3 启动的 GenServer 与父进程链接,如果父进程崩溃,它将退出。如果 GenServer 配置为在 init/1 回调中捕获退出,则 GenServer 也将因 :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} 是活动的,因为进程可能在查找后立即终止。

示例

例如,要查找服务器进程,监视它并向它发送广播

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