檢視原始碼 代理程式 (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, [%{}]}}}
代理的狀態將新增至指定的參數清單 ([%{}]
) 作為第一個參數。
摘要
函式
對代理狀態執行 cast(fire and forget)操作。
對代理狀態執行 cast(fire and forget)操作。
傳回一個規格,以在監督下啟動代理。
透過指定的匿名函式取得代理值。
透過指定的函式取得代理值。
透過指定的匿名函式,在一次操作中取得並更新代理狀態。
透過指定的函式,在一次操作中取得並更新代理狀態。
啟動沒有連結的代理程序(在監督樹之外)。
使用指定的模組、函式和引數啟動沒有連結的代理。
使用指定的函式啟動連結到目前程序的代理。
啟動連結到目前程序的代理。
使用指定的 reason
同步停止代理。
透過指定的匿名函式更新代理狀態。
透過指定的函式更新代理狀態。
類型
函式
對代理狀態執行 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(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
傳回一個規格,以在監督下啟動代理。
有關更詳細的資訊,請參閱 Supervisor
模組中的「Child specification」區段。
透過指定的匿名函式取得代理值。
函式 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/3
相同,但預期的是模組、函式和引數,而不是匿名函式。狀態會新增為引數清單中的第一個引數。
透過指定的匿名函式,在一次操作中取得並更新代理狀態。
函式 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/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()
使用指定的函式啟動連結到目前程序的代理。
這通常用於將 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"}
@spec start_link(module(), atom(), [any()], GenServer.options()) :: on_start()
啟動連結到目前程序的代理。
與 start_link/2
相同,但預期的是模組、函式和引數,而不是匿名函式;module
中的 fun
會使用給定的引數 args
來呼叫,以初始化狀態。
使用指定的 reason
同步停止代理。
如果代理程式以給定的原因終止,則會傳回 :ok
。如果代理程式以其他原因終止,則呼叫會結束。
此函式會保留 OTP 語意,關於錯誤回報。如果原因是 :normal
、:shutdown
或 {:shutdown, _}
以外的任何原因,則會記錄錯誤報告。
範例
iex> {:ok, pid} = Agent.start_link(fn -> 42 end)
iex> Agent.stop(pid)
: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/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