代理是一个围绕状态的简单抽象。
在 Elixir 中,通常需要共享或存储状态,这些状态必须从不同的进程访问,或者由同一进程在不同时间点访问。
Agent
模块提供了一个简单的服务实现,允许通过简单的API访问和更新状态。
例子
举个例子,下面的代理实现了一个计数器:
defmodule Counter do
use Agent
def start_link(initial_value) do
Agent.start_link(fn -> initial_value end, name: __MODULE__)
end
def value do
Agent.get(__MODULE__, & &1)
end
def increment do
Agent.update(__MODULE__, &(&1 + 1))
end
end
使用方式如下:
Counter.start_link(0)
#=> {:ok, #PID<0.123.0>}
Counter.value()
#=> 0
Counter.increment()
#=> :ok
Counter.increment()
#=> :ok
Counter.value()
#=> 2
得益于代理服务进程,计数器可以安全的并发增加。
使用
use Agent
时,Agent
模块会定义一个child_spec/1
函数,因此你的模块可以作为监督数的一个子节点。
代理在客户端和服务API之间提供了一种隔离,类似于 GenServer
。具体来说,调用 Agent
函数时作为参数传递的函数是在代理(服务器)内被调用的。理解这点很重要,因为你应该避免在代理内执行昂贵的操作,因为它会阻塞代理直到请求结束。
考虑下面的两个例子:
# Compute in the agent/server
def get_something(agent) do
Agent.get(agent, fn state -> do_something_expensive(state) end)
end
# Compute in the agent/client
def get_something(agent) do
Agent.get(agent, & &1) |> do_something_expensive()
end
第一个函数会阻塞代理。第二个函数将状态拷贝到客户端然后在客户端执行操作。需要考虑的一个方面是数据是否大到需要在服务端处理,或者小到可以廉价地发送到客户端。另一个点是数据是否需要被原子地处理:获取数据并在代理外调用 do_something_expensive(state)
意味着状态可能在此过程中被修改。这在更新是很重要,因为如果在客户端而不是服务端计算新状态,当多个客户端试图将同一状态更新为不同值时可能会导致竞态条件。
监督
Agent
通常在监督树下启动。当我们调用 use Agent
时,它会自动定义一个 child_spec/1
函数,它让我们可以直接在监督树下启动代理。例如我们在监督树下使用初始值0启动代理:
children = [
{Counter, 0}
]
Supervisor.start_link(children, strategy: :one_for_all)
你也可以简单的将 Counter
作为子节点传递给监督者,例如:
children = [
Counter # Same as {Counter, []}
]
Supervisor.start_link(children, strategy: :one_for_all)
上面的代码不适用于我们的例子,因为它默认使用空列表作为初始值启动计数器。但是你可以在自己的代理中使用。一个通常的做法是使用关键字列表,它可以同时设置初始值并给计数器进程一个名称,例如:
def start_link(opts) do
{initial_value, opts} = Keyword.pop(opts, :initial_value, 0)
Agent.start_link(fn -> initial_value end, opts)
end
然后,你就可以使用 Counter
, {Counter, name: :my_counter}
或者 {Counter, initial_value: 0, name: :my_counter}
作为子节点。
use Agent
也接受一系列选项,用来配置子节点如何在监督者下运行。生成的 child_spec/1
支持以下自定义选项:
:id
- 子节点标识符,默认是当前模块:restart
- 当子节点需要重启时指定,默认:permanent
:shutdown
- 如何停止子节点,要么立即停止,要么给一段时间停止
举个例子:
use Agent, restart: :transient, shutdown: 10_000
紧接在 use Agent
之前的 @doc
注解将附加到生成的 child_spec/1
函数上。
分布式代理
分布式代理也有限制。代理提供了两个API,一个接受匿名函数,另一个接受模块,函数和参数。
在具有多个节点的分布式环境中,接受匿名函数的API要求调用者(客户端)和代理有相同版本的调用者模块。
注意这个问题在进行代理”滚动升级“时也会出现。滚动升级是指:关闭一些节点并使用运行新版本软件的节点替代,以此来升级软件版本。在这种情况下,你环境的一部分会具有指定版本的模块,而另一部分具有相同模块的其他版本(更新的版本)。
最好的解决办法是在分布式代理中使用指定模块,函数和参数的API。
热代码交换
代理可以通过给更新指令传递一个模块,函数和参数的元组来实时热更新代码。例如,假设你有一个名为 :sample
的代理,你希望将它的内部状态从关键字列表转换成map。可以使用下面的命令:
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
代理的状态将作为给定参数列表([%{}]
)的第一个参数。