Elixir Agent

代理是一个围绕状态的简单抽象。

在 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, [%{}]}}}

代理的状态将作为给定参数列表([%{}])的第一个参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值