當前位置: 首頁>>代碼示例 >>用法及示例精選 >>正文


Elixir Task用法及代碼示例


Elixir語言中 Task 相關用法介紹如下。

產生和等待任務的便利。

任務是旨在在其整個生命周期中執行一項特定操作的進程,通常與其他進程很少或根本沒有通信。任務最常見的用例是通過異步計算值將順序代碼轉換為並發代碼:

task = Task.async(fn -> do_some_work() end)
res = do_some_other_work()
res + Task.await(task)

使用async 生成的任務可以由它們的調用者進程(並且隻有它們的調用者)等待,如上麵的示例所示。它們是通過生成一個進程來實現的,該進程在執行給定的計算後向調用者發送消息。

除了 async/1 await/2 之外,任務還可以作為監督樹的一部分啟動並在遠程節點上動態生成。接下來我們將探討這些場景。

異步和等待

任務的常見用途之一是使用 Task.async/1 將順序代碼轉換為並發代碼,同時保持其語義。調用時,調用者將創建、鏈接和監視一個新進程。任務操作完成後,將向調用者發送一條帶有結果的消息。

Task.await/2 用於讀取任務發送的消息。

使用 async 時需要考慮兩件重要的事情:

  1. 如果您使用的是異步任務,則必須等待回複,因為它們是 always 發送的。如果您不期待回複,請考慮使用 Task.start_link/1 ,詳情如下。

  2. 異步任務鏈接調用者和生成的進程。這意味著,如果調用者崩潰,任務也會崩潰,反之亦然。這是有目的的:如果要接收結果的過程不再存在,那麽完成計算就沒有目的了。

    如果不希望這樣做,您將需要使用監督任務,如下所述。

動態監督任務

Task.Supervisor 模塊允許開發人員動態創建多個監督任務。

一個簡短的例子是:

{:ok, pid} = Task.Supervisor.start_link()

task =
  Task.Supervisor.async(pid, fn ->
    # Do something
  end)

Task.await(task)

但是,在大多數情況下,您希望將任務主管添加到您的監督樹中:

Supervisor.start_link([
  {Task.Supervisor, name: MyApp.TaskSupervisor}
], strategy: :one_for_one)

現在您可以通過傳遞主管的名稱而不是 pid 來使用 async/await:

Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
  # Do something
end)
|> Task.await()

我們鼓勵開發人員盡可能多地依賴受監督的任務。受監督的任務支持多種模式,允許您明確控製如何處理結果、錯誤和超時。這是一個摘要:

有關支持的操作的詳細信息,請參閱 Task.Supervisor 模塊。

分布式任務

由於 Elixir 提供了 Task.Supervisor ,因此很容易使用它來跨節點動態啟動任務:

# On the remote node
Task.Supervisor.start_link(name: MyApp.DistSupervisor)

# On the client
supervisor = {MyApp.DistSupervisor, :remote@local}
Task.Supervisor.async(supervisor, MyMod, :my_fun, [arg1, arg2, arg3])

請注意,在處理分布式任務時,應該使用需要顯式模塊、函數和參數的 Task.Supervisor.async/5 函數,而不是使用匿名函數的 Task.Supervisor.async/3 。這是因為匿名函數期望所有涉及的節點上都存在相同的模塊版本。查看 Agent 模塊文檔以獲取有關分布式進程的更多信息,因為那裏說明的限製適用於整個生態係統。

靜態監督任務

Task 模塊實現了 child_spec/1 函數,這允許它直接在常規 Supervisor 下啟動 - 而不是 Task.Supervisor - 通過傳遞帶有要運行的函數的元組:

Supervisor.start_link([
  {Task, fn -> :some_work end}
], strategy: :one_for_one)

當您需要在設置監督樹時執行某些步驟時,這通常很有用。例如:預熱緩存、記錄初始化狀態等。

如果您不想將任務代碼直接放在 Supervisor 下,可以將 Task 包裝在其自己的模塊中,類似於使用 GenServer Agent 的方式:

defmodule MyTask do
  use Task

  def start_link(arg) do
    Task.start_link(__MODULE__, :run, [arg])
  end

  def run(arg) do
    # ...
  end
end

然後將其傳遞給主管:

Supervisor.start_link([
  {MyTask, arg}
], strategy: :one_for_one)

由於這些任務是受監督的,並且不直接與調用者相關聯,因此無法等待它們。默認情況下,函數 Task.start/1 Task.start_link/1 用於fire-and-forget 任務,您不關心結果或它是否成功完成。

use Task 定義了一個 child_spec/1 函數,允許將定義的模塊放在監督樹下。生成的 child_spec/1 可以使用以下選項進行自定義:

  • :id - 子規範標識符,默認為當前模塊
  • :restart - 當孩子應該重新啟動時,默認為 :temporary
  • :shutdown - 如何立即關閉孩子,或者讓孩子有時間關閉

GenServer Agent Supervisor 相對,任務的默認 :restart:temporary 。這意味著即使任務崩潰也不會重新啟動任務。如果您希望在不成功退出時重新啟動任務,請執行以下操作:

use Task, restart: :transient

如果您希望任務始終重新啟動:

use Task, restart: :permanent

有關更多詳細信息,請參閱 Supervisor 模塊中的"Child specification" 部分。緊接在use Task 之前的@doc 注釋將附加到生成的 child_spec/1 函數。

祖先和調用者跟蹤

每當您啟動一個新進程時,Elixir 都會通過進程字典中的 $ancestors 鍵對該進程的父進程進行注釋。這通常用於跟蹤監督樹內的層次結構。

例如,我們建議開發人員始終在主管的指導下啟動任務。這提供了更多可見性,並允許您控製在節點關閉時如何終止這些任務。這可能看起來像Task.Supervisor.start_child(MySupervisor, task_function).這意味著,盡管您的代碼是調用任務的人,但任務的實際祖先是主管,因為主管是有效啟動它的人。

為了跟蹤您的代碼和任務之間的關係,我們使用進程字典中的$callers 鍵。因此,假設上麵的 Task.Supervisor 調用,我們有:

[your code] -- calls --> [supervisor] ---- spawns --> [task]

這意味著我們存儲以下關係:

[your code]              [supervisor] <-- ancestor -- [task]
    ^                                                  |
    |--------------------- caller ---------------------|

可以使用 Process.get(:"$callers") 從 Process 字典中檢索當前進程的調用者列表。這將返回 nil 或帶有至少一個條目的列表 [pid_n, ..., pid2, pid1] 其中 pid_n 是調用當前進程的 PID,pid2 調用 pid_npid2pid1 調用。

如果任務崩潰,調用者字段將作為日誌消息元數據的一部分包含在 :callers 鍵下。

相關用法


注:本文由純淨天空篩選整理自elixir-lang.org大神的英文原創作品 Task。非經特殊聲明,原始代碼版權歸原作者所有,本譯文未經允許或授權,請勿轉載或複製。