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


Elixir Agent用法及代码示例


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

由于代理服务器进程,计数器可以安全地同时递增。

代理提供客户端和服务器 API 之间的隔离(类似于 GenServer s)。特别是,作为参数传递给 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

有关更多详细信息,请参阅 Supervisor 模块中的"Child specification" 部分。紧接在use Agent 之前的@doc 注释将附加到生成的 child_spec/1 函数。

名称注册

代理绑定到与 GenServer 相同的名称注册规则。在 GenServer 文档中阅读更多相关信息。

关于分布式代理的一句话

考虑分布式代理的局限性很重要。代理提供了两种 API,一种用于匿名函数,另一种需要显式模块、函数和参数。

在具有多个节点的分布式设置中,接受匿名函数的 API 仅在调用者(客户端)和代理具有相同版本的调用者模块时才有效。

请记住,在使用代理执行 "rolling upgrades" 时也会出现此问题。滚动升级是指以下情况:您希望通过shutting down 部署您的一些节点的新版本软件,并用运行新版本软件的节点替换它们。在此设置中,您的环境的一部分将具有给定模块的一个版本,而另一部分将具有同一模块的另一个版本(较新的版本)。

最好的解决方案是在使用分布式代理时简单地使用显式模块、函数和参数 API。

热代码交换

代理可以通过简单地将模块、函数和参数元组传递给更新指令来使其代码实时热交换。例如,假设您有一个名为 :sample 的代理,并且您希望将其内部状态从关键字列表转换为Map。可以通过以下指令完成:

{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}

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

相关用法


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