查看源代码 Agent (Elixir v1.16.2)
代理是对状态的简单抽象。
在 Elixir 中,通常需要共享或存储必须从不同进程访问或在不同时间点由同一进程访问的状态。
The 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
当您
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)
意味着代理的状态可能在此期间更新。这在更新的情况下尤其重要,因为在客户端而不是服务器中计算新状态会导致竞争条件,如果多个客户端尝试将相同状态更新为不同的值。
如何监督
An 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
模块中的“子项规范”部分。紧接在 use Agent
之前的 @doc
注释将附加到生成的 child_spec/1
函数。
名称注册
代理与 GenServer 绑定相同的名称注册规则。在 GenServer
文档中了解有关它的更多信息。
关于分布式代理的一句话
重要的是要考虑分布式代理的局限性。代理提供了两个 API,一个用于匿名函数,另一个用于显式模块、函数和参数。
在具有多个节点的分布式环境中,接受匿名函数的 API 仅在调用者(客户端)和代理具有调用者模块的相同版本时才有效。
请记住,此问题在使用代理执行“滚动升级”时也会出现。滚动升级是指以下情况:您希望通过 _关闭_ 一些节点并将它们替换为运行软件新版本的节点来部署软件的新版本。在这种情况下,您环境的一部分将具有给定模块的一个版本,而另一部分将具有相同模块的另一个版本(较新的版本)。
最佳解决方案是在使用分布式代理时简单地使用显式模块、函数和参数 API。
热代码交换
可以通过简单地将模块、函数和参数元组传递给更新指令来实时热交换代理的代码。例如,假设您有一个名为 :sample
的代理,并且您想将其内部状态从关键字列表转换为映射。可以使用以下指令完成此操作
{:update, :sample, {:advanced, {Enum, :into, [%{}]}}}
代理的状态将作为第一个参数添加到给定的参数列表 ([%{}]
) 中。
总结
函数
对代理状态执行投递(_发后即忘_)操作。
对代理状态执行投递(_发后即忘_)操作。
返回在监督者下启动代理的规范。
通过给定的匿名函数获取代理值。
通过给定的函数获取代理值。
通过给定的匿名函数在一个操作中获取和更新代理状态。
通过给定的函数在一个操作中获取和更新代理状态。
启动一个没有链接的代理进程(在监督树之外)。
使用给定的模块、函数和参数启动一个没有链接的代理。
使用给定的函数启动一个链接到当前进程的代理。
启动一个链接到当前进程的代理。
使用给定的 reason
同步停止代理。
通过给定的匿名函数更新代理状态。
通过给定的函数更新代理状态。
类型
函数
对代理状态执行投递(_发后即忘_)操作。
函数 fun
被发送到 agent
,它调用该函数,并将代理状态作为参数传递。 fun
的返回值成为代理的新状态。
请注意,cast
会立即返回 :ok
,而不管 agent
(或它应该所在的节点)是否存在。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.cast(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43
对代理状态执行投递(_发后即忘_)操作。
与 cast/2
相同,但需要模块、函数和参数,而不是匿名函数。状态将作为第一个参数添加到给定的参数列表中。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.cast(pid, Kernel, :+, [12])
:ok
iex> Agent.get(pid, fn state -> state end)
54
返回在监督者下启动代理的规范。
有关详细信息,请参阅 Supervisor
模块中的“子项规范”部分。
通过给定的匿名函数获取代理值。
函数 fun
被发送到 agent
,它调用该函数,并将代理状态作为参数传递。函数调用的结果将从此函数返回。
timeout
是一个大于零的整数,它指定在代理执行函数并返回结果值之前允许的毫秒数,或者原子 :infinity
表示无限期等待。如果在指定时间内未收到任何结果,则函数调用失败,并且调用者退出。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
通过给定的函数获取代理值。
与 get/3
相同,但需要模块、函数和参数,而不是匿名函数。状态将作为第一个参数添加到给定的参数列表中。
通过给定的匿名函数在一个操作中获取和更新代理状态。
函数 fun
被发送到 agent
,它调用该函数,并将代理状态作为参数传递。该函数必须返回一个包含两个元素的元组,第一个元素是要返回的值(即“获取”值),第二个元素是代理的新状态。
timeout
是一个大于零的整数,它指定在代理执行函数并返回结果值之前允许的毫秒数,或者原子 :infinity
表示无限期等待。如果在指定时间内未收到任何结果,则函数调用失败,并且调用者退出。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get_and_update(pid, fn state -> {state, state + 1} end)
42
iex> Agent.get(pid, fn state -> state end)
43
通过给定的函数在一个操作中获取和更新代理状态。
与 get_and_update/3
相同,但需要模块、函数和参数,而不是匿名函数。状态将作为第一个参数添加到给定的参数列表中。
@spec start((-> term()), GenServer.options()) :: on_start()
启动一个没有链接的代理进程(在监督树之外)。
有关详细信息,请参阅 start_link/2
。
示例
iex> {:ok, pid} = Agent.start(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
@spec start(module(), atom(), [any()], GenServer.options()) :: on_start()
使用给定的模块、函数和参数启动一个没有链接的代理。
有关详细信息,请参阅 start_link/4
。
@spec start_link((-> term()), GenServer.options()) :: on_start()
使用给定的函数启动一个链接到当前进程的代理。
这通常用于将代理作为监督树的一部分启动。
代理生成后,给定函数 fun
将在服务器进程中调用,并应返回代理的初始状态。请注意,start_link/2
不会返回,直到给定函数返回。
选项
使用 :name
选项进行注册,如模块文档中所述。
如果存在 :timeout
选项,则代理最多允许花费给定毫秒数进行初始化,否则它将被终止,并且启动函数将返回 {:error, :timeout}
。
如果存在 :debug
选项,则将调用 :sys
模块 中的相应函数。
如果存在 :spawn_opt
选项,其值将作为选项传递给底层进程,就像在 Process.spawn/4
中一样。
返回值
如果服务器成功创建并初始化,则该函数返回 {:ok, pid}
,其中 pid
是服务器的 PID。如果具有指定名称的代理已经存在,则该函数将返回 {:error, {:already_started, pid}}
,其中包含该进程的 PID。
如果给定的函数回调失败,则该函数返回 {:error, reason}
。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
iex> {:error, {exception, _stacktrace}} = Agent.start(fn -> raise "oops" end)
iex> exception
%RuntimeError{message: "oops"}
@spec start_link(module(), atom(), [any()], GenServer.options()) :: on_start()
启动一个链接到当前进程的代理。
与 start_link/2
相同,但需要一个模块、函数和参数,而不是匿名函数;fun
在 module
中将使用给定的参数 args
被调用以初始化状态。
使用给定的 reason
同步停止代理。
如果代理以给定的原因终止,则返回 :ok
。如果代理以其他原因终止,则调用将退出。
此函数保留了有关错误报告的 OTP 语义。如果原因不是 :normal
、:shutdown
或 {:shutdown, _}
,则会记录错误报告。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.stop(pid)
:ok
通过给定的匿名函数更新代理状态。
函数 fun
被发送到 agent
,它调用该函数,并将代理状态作为参数传递。 fun
的返回值成为代理的新状态。
此函数始终返回 :ok
。
timeout
是一个大于零的整数,它指定在代理执行函数并返回结果值之前允许的毫秒数,或者原子 :infinity
表示无限期等待。如果在指定时间内未收到任何结果,则函数调用失败,并且调用者退出。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, fn state -> state + 1 end)
:ok
iex> Agent.get(pid, fn state -> state end)
43
通过给定的函数更新代理状态。
与 update/3
相同,但需要一个模块、函数和参数,而不是匿名函数。状态作为第一个参数添加到给定的参数列表中。
示例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.update(pid, Kernel, :+, [12])
:ok
iex> Agent.get(pid, fn state -> state end)
54