檢視原始碼 代理程式 (Elixir v1.16.2)

代理程式是狀態周圍的簡單抽象。

在 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

當您 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

請參閱 Supervisor 模組中的「子項規格」區段,以取得更詳細的資訊。緊接在 use Agent 之前的 @doc 標註會附加到產生的 child_spec/1 函式。

名稱註冊

代理與 GenServers 繫結至相同的名稱註冊規則。在 GenServer 文件中深入了解。

分布式代理的一句話

考量分布式代理的限制非常重要。代理提供兩個 API,一個使用匿名函式,另一個則預期明確的模組、函式和參數。

在具有多個節點的分布式設定中,只接受匿名函式的 API 僅在呼叫者 (客戶端) 和代理具有相同版本的呼叫者模組時才有效。

請記住,在使用代理執行「滾動升級」時,也會出現這個問題。滾動升級是指以下情況:您希望透過關閉部分節點,並以執行軟體新版本的節點取代它們,來部署軟體的新版本。在此設定中,您的環境的一部分將具有特定模組的一個版本,而另一部分則具有同一個模組的另一個版本 (較新的版本)。

最佳的解決方案是在使用分布式代理時,僅使用明確的模組、函式和參數 API。

熱程式碼交換

代理可以透過傳遞模組、函式和參數組給更新指令,讓其程式碼熱交換。例如,想像您有一個名為 :sample 的代理,而且您想將其內部狀態從關鍵字清單轉換為映射。可以使用以下指令執行此動作

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

代理的狀態將新增至指定的參數清單 ([%{}]) 作為第一個參數。

摘要

類型

代理參考

代理名稱

start* 函式的傳回值

代理狀態

函式

對代理狀態執行 cast(fire and forget)操作。

對代理狀態執行 cast(fire and forget)操作。

傳回一個規格,以在監督下啟動代理。

透過指定的匿名函式取得代理值。

透過指定的函式取得代理值。

透過指定的匿名函式,在一次操作中取得並更新代理狀態。

透過指定的函式,在一次操作中取得並更新代理狀態。

啟動沒有連結的代理程序(在監督樹之外)。

使用指定的模組、函式和引數啟動沒有連結的代理。

使用指定的函式啟動連結到目前程序的代理。

啟動連結到目前程序的代理。

使用指定的 reason 同步停止代理。

透過指定的匿名函式更新代理狀態。

透過指定的函式更新代理狀態。

類型

@type agent() :: pid() | {atom(), node()} | name()

代理參考

@type name() :: atom() | {:global, term()} | {:via, module(), term()}

代理名稱

@type on_start() :: {:ok, pid()} | {:error, {:already_started, pid()} | term()}

start* 函式的傳回值

@type state() :: term()

代理狀態

函式

@spec cast(agent(), (state() -> state())) :: :ok

對代理狀態執行 cast(fire and forget)操作。

函式 fun 會傳送至 agent,而 agent 會呼叫函式並傳遞代理狀態。 fun 的傳回值會成為代理的新狀態。

請注意,無論 agent(或其應該存在的節點)是否存在,cast 都會立即傳回 :ok

範例

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(agent, module, fun, args)

檢視來源
@spec cast(agent(), module(), atom(), [term()]) :: :ok

對代理狀態執行 cast(fire and forget)操作。

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
連結到此函式

child_spec(arg)

檢視原始碼 (自 1.5.0 起)

傳回一個規格,以在監督下啟動代理。

有關更詳細的資訊,請參閱 Supervisor 模組中的「Child specification」區段。

連結到此函式

get(agent, fun, timeout \\ 5000)

檢視來源
@spec get(agent(), (state() -> a), timeout()) :: a when a: var

透過指定的匿名函式取得代理值。

函式 fun 會傳送至 agent,後者會呼叫函式並傳遞 agent 狀態。此函式會傳回函式呼叫的結果。

timeout 是大於 0 的整數,用來指定 agent 在執行函式並傳回結果值之前允許經過的毫秒數,或原子 :infinity 以無限期等待。如果在指定時間內未收到結果,函式呼叫會失敗,且呼叫者會結束。

範例

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.get(pid, fn state -> state end)
42
連結到此函式

get(agent, module, fun, args, timeout \\ 5000)

檢視來源
@spec get(agent(), module(), atom(), [term()], timeout()) :: any()

透過指定的函式取得代理值。

get/3 相同,但預期的是模組、函式和引數,而不是匿名函式。狀態會新增為引數清單中的第一個引數。

連結到此函式

get_and_update(agent, fun, timeout \\ 5000)

檢視來源
@spec get_and_update(agent(), (state() -> {a, state()}), timeout()) :: a when a: var

透過指定的匿名函式,在一次操作中取得並更新代理狀態。

函式 fun 會傳送至 agent,後者會呼叫函式並傳遞 agent 狀態。函式必須傳回包含兩個元素的元組,第一個元素為要傳回的值(亦即「get」值),第二個元素為 agent 的新狀態。

timeout 是大於 0 的整數,用來指定 agent 在執行函式並傳回結果值之前允許經過的毫秒數,或原子 :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(agent, module, fun, args, timeout \\ 5000)

檢視來源
@spec get_and_update(agent(), module(), atom(), [term()], timeout()) :: any()

透過指定的函式,在一次操作中取得並更新代理狀態。

get_and_update/3 相同,但預期的是模組、函式和引數,而不是匿名函式。狀態會新增為引數清單中的第一個引數。

連結到此函式

start(fun, options \\ [])

檢視來源
@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
連結到此函式

start(module, fun, args, options \\ [])

檢視來源
@spec start(module(), atom(), [any()], GenServer.options()) :: on_start()

使用指定的模組、函式和引數啟動沒有連結的代理。

有關更多資訊,請參閱 start_link/4

連結到此函式

start_link(fun, options \\ [])

檢視來源
@spec start_link((-> term()), GenServer.options()) :: on_start()

使用指定的函式啟動連結到目前程序的代理。

這通常用於將 agent 作為監督樹的一部分啟動。

一旦 agent 產生,所提供的函式 fun 會在伺服器程序中呼叫,並應傳回初始 agent 狀態。請注意,start_link/2 在所提供的函式傳回之前不會傳回。

選項

:name 選項用於註冊,如模組文件所述。

如果存在 :timeout 選項,則允許 agent 花費最多指定的毫秒數進行初始化,否則將會終止 agent,且啟動函式會傳回 {: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"}
連結到此函式

start_link(module, fun, args, options \\ [])

檢視來源
@spec start_link(module(), atom(), [any()], GenServer.options()) :: on_start()

啟動連結到目前程序的代理。

start_link/2 相同,但預期的是模組、函式和引數,而不是匿名函式;module 中的 fun 會使用給定的引數 args 來呼叫,以初始化狀態。

連結到此函式

stop(agent, reason \\ :normal, timeout \\ :infinity)

檢視來源
@spec stop(agent(), reason :: term(), timeout()) :: :ok

使用指定的 reason 同步停止代理。

如果代理程式以給定的原因終止,則會傳回 :ok。如果代理程式以其他原因終止,則呼叫會結束。

此函式會保留 OTP 語意,關於錯誤回報。如果原因是 :normal:shutdown{:shutdown, _} 以外的任何原因,則會記錄錯誤報告。

範例

iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.stop(pid)
:ok
連結到此函式

update(agent, fun, timeout \\ 5000)

檢視來源
@spec update(agent(), (state() -> state()), timeout()) :: :ok

透過指定的匿名函式更新代理狀態。

函式 fun 會傳送至 agent,而 agent 會呼叫函式並傳遞代理狀態。 fun 的傳回值會成為代理的新狀態。

此函式總是傳回 :ok

timeout 是大於 0 的整數,用來指定 agent 在執行函式並傳回結果值之前允許經過的毫秒數,或原子 :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(agent, module, fun, args, timeout \\ 5000)

檢視來源
@spec update(agent(), module(), atom(), [term()], timeout()) :: :ok

透過指定的函式更新代理狀態。

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