GenServer(通用服务器)是一种行为,它抽象了 Elixir 进程之间常见的客户端-服务器交互。
还记得我们在学习进程时讲到的接收循环吗?GenServer行为为实现这类循环,以及与运行这类循环的进程交换消息提供了抽象。它让保存状态和执行异步代码变得更容易。
要注意,GenServer 这个名字含义很多。它既可以指_使用_ GenServer行为的_模块_,也可以指从_使用_ GenServer行为的模块启动的_进程_。
GenServer行为定义了一个必需的回调 init/1,以及几个有意思的可选回调:handle_call/3、handle_cast/2 和 handle_info/3。使用 GenServer 的_客户端_不应该直接调用这些回调。GenServer模块提供了一些函数,客户端可以用它们与 GenServer进程通信。
通常,单个模块会同时定义_客户端 API_(一组函数,Elixir 应用的其他部分可以调用它们来与这个 GenServer进程通信)和_服务器回调实现_(包含这个 GenServer 的逻辑)。
我们先看一个简单的 GenServer 示例,然后再了解每个回调的含义。
这是一个示例服务器,用来应对长途公路旅行中烦人乘客没完没了的追问,更确切地说,是这个问题:“到了没有?”。它会记录这个问题被问了多少次,并给出越来越不耐烦的回应。
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1可以通过调用 GenServer.start/3 或 GenServer.start_link/3 来启动一个服务器。我们在链接概念中了解了这些函数的区别。
这两个函数:
GenServer行为的模块。init_arg,可以接受任何值。顾名思义,这个参数会被传给 init/1回调。调用 GenServer.start/3 或 GenServer.start_link/3 启动服务器时,会以阻塞的方式调用 init/1回调。init/1 的返回值决定服务器能否成功启动。
init/1回调通常返回下列值之一:
{:ok, state}。服务器会以 state 作为初始状态启动接收循环。state 可以是任何类型。{:stop, reason}。reason 可以是任何类型。服务器不会启动接收循环。进程会以给定的原因退出。还有一些更高级的用法,这里先不涉及。
如果服务器的接收循环启动了,GenServer.start/3 和 GenServer.start_link/3 这两个函数会返回 {:ok, pid}元组。否则返回 {:error, reason}。
handle_call/3需要回复的消息可以用 GenServer.call/2 发送给服务器进程。这个函数的第一个参数是运行中的服务器进程的 pid,第二个参数是消息。消息可以是任何类型。
handle_call/3回调负责处理和响应同步消息。它接收三个参数:
message:传给 GenServer.call/2 第二个参数的值。from:调用 GenServer.call/2 的进程的 pid。多数情况下这个参数可以忽略。state:服务器的当前状态。记住,它的初始值是在 init/1回调中设置的。handle_call/3回调通常返回一个三元组 {:reply, reply, state}。这意味着元组中的第二个元素 reply(可以是任何类型)会被送回调用方。元组中的第三个元素 state 是服务器处理完这条消息后的新状态。
还有一些更高级的用法,这里先不涉及。
要凭名字记住这个回调的作用, 可以把它想象成在电话里“呼叫”某人。
如果那个人有空,你会立刻(同步地)收到回复。
handle_cast/2不需要回复的消息可以用 GenServer.cast/2 发送给服务器进程。它的参数与 GenServer.call/2 完全相同。
handle_cast/2回调负责处理这类消息。它接收两个参数 message 和 state,与 handle_call/3回调中的参数相同(只是没有 from)。
handle_cast/2回调通常返回一个二元组 {:noreply, state}。
还有一些更高级的用法,这里先不涉及。
要凭名字记住这个回调的作用, 可以记住 “cast” 也有“扔”的意思。
如果你把装有消息的瓶子扔进大海, 不会指望立刻收到回复, 也许永远都收不到。
call 还是 cast?即使客户端代码不需要服务器的回复,也几乎总是应该用 call。
使用 call 会等待回复,这是一种背压机制(防止客户端一次发送过多消息)。收到服务器的回复也是唯一能确定服务器收到并处理了客户端消息的方式。
handle_info/2消息也可能通过调用 GenServer.call/2 或 GenServer.cast/2 以外的方式进入服务器的收件箱,例如直接调用 send/2 函数。
要处理这类消息,请使用 handle_info/2回调。这个回调的工作方式与 handle_cast/2 完全相同。
GenServer行为为 handle_info/2 提供了一个兜底实现,会记录关于意外消息的错误。如果你覆盖了这个默认实现,一定要始终包含自己的兜底实现。如果忘了,服务器在收到意外消息时会崩溃。
上面四个回调的返回值都可以再扩展一个元组元素:超时时间。例如,init/1 可以不返回 {:ok, state},而是返回 {:ok, state, timeout}。
超时时间可以用来检测邮箱中在一段特定时间内没有消息。如果服务器从某个回调中返回了超时时间,而在指定的毫秒数内没有收到任何消息,就会以 :timeout 作为第一个参数调用 handle_info/2。