任務

任務 在 Elixir

2 個練習

關於 任務

Task 是一種用來執行單一特定操作的行程。 它們通常不會和其他行程溝通,但可以把結果回傳給啟動該 Task 的行程。

Task 常用來將工作平行化。

async/await

要啟動一個 Task,請使用 Task.async/1。它接受一個匿名函式作為引數,並在一個連結到呼叫端行程的新行程中執行它。它會回傳一個 %Task{} 結構。

要取得執行結果,請把 %Task{} 結構傳給 Task.await/2。它會等待 Task 完成並回傳結果。第二個引數是逾時時間,單位為毫秒,預設值為 5000。可以把 :infinity 當作逾時值傳入,讓 Task 不受逾時限制地執行。

task = Task.async(fn -> Enum.sum(0..999_999) end)
# => %Task{
#      owner: #PID<0.110.0>,
#      pid: #PID<0.119.0>,
#      ref: #Reference<0.3715954046.815529991.83334>
#    }

Task.await(task)
# => 499999500000

請注意,在啟動 Task 到等待 Task 之間,啟動該 Task 的行程不會被阻塞,可以繼續進行其他操作。

用 Task.async/1 啟動的任何 Task 都應該被等待,因為它會傳送訊息給呼叫端的行程。每個 Task 只能呼叫 Task.await/2 一次。

如果你只想為了副作用而啟動 Task,請使用 Task.start/1 或 Task.start_link/1。Task.start/1 會啟動一個未連結到呼叫端行程的 Task,而 Task.start_link/1 則會啟動一個連結到呼叫端行程的 Task。這兩個函式都會回傳一個 {:ok, pid} 元組。

Timeouts

常見的模式是先啟動非同步的 Task,再用 Enum.map/2 等待它們,如下所示:

inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Enum.map(&Task.await(&1, 1_000))

要注意 Task.await 是同步的,而且每次呼叫時,它的逾時計時都會從 0 重新開始。這表示當它以這種方式循序使用時,逾時時間會不斷累加。

第一個被等待的 Task 執行時間不會超過逾時上限,但第二個被等待的 Task 則可以執行「第一個 Task 的執行時間加上逾時上限」那麼久。

設想一種情況:每個 Task 的執行時間都比前一個稍微長一點,但執行時間的差距小於等待的逾時上限。

inputs = 1..10
function = fn n -> :timer.sleep(n * 900) && n end

我們可以用 :timer.tc/1 這個 Erlang 函式來測量這些 Task 會執行多久,單位是微秒。

:timer.tc(fn ->
  inputs
  |> Enum.map(&Task.async(fn -> function.(&1) end))
  |> Enum.map(&Task.await(&1, 1_000))
end)

# => {9004517, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}

9004517 微秒等於 9 秒,比給定的逾時上限 1 秒(1000 毫秒)多出許多。

如果這正是你想要的行為,那很好,放手去做吧!如果不是,替代方案是使用 Task.async_stream/3 或 Task.await_many/2。

Task.async_stream/3

Task.async_stream/3 讓你用同一個函式處理許多不同的輸入,每個輸入各自在自己的 Task 中執行。這些 Task 會連結到目前的行程,就像用 Task.async/1 執行時一樣。它會回傳一個串流。

它接受一個逾時值,分別套用到每一個 Task。以相同的輸入和相同的函式改用 Task.async_stream/3 來看看:

inputs
|> Task.async_stream(function, timeout: 1_000)
|> Enum.to_list()

# => ** (exit) exited in: Task.Supervised.stream(1000)
#       ** (EXIT) time out
#       (elixir 1.11.3) lib/task/supervised.ex:304: Task.Supervised.stream_reduce/7
#       (elixir 1.11.3) lib/enum.ex:3473: Enum.reverse/1
#       (elixir 1.11.3) lib/enum.ex:3066: Enum.to_list/1

如果你只有一個函式要套用到不同輸入上,Task.async_stream/3 非常好用。它接受其他實用的選項,提供很大的控制彈性,例如同時執行多少個 Task,以及 Task 逾時時要怎麼處理。

如果你需要執行的 Task 各自做不同的事,可能就得改用 Task.await_many/2。

Task.await_many/2

Task.await_many/2 是在 Elixir 1.11 中推出的。它讓你用一個共用的逾時值等待多個 Task。以相同的輸入和相同的函式改用 Task.await_many/2 來看看:

inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Task.await_many(1_000)

# => ** (exit) exited in: Task.await_many([...], 1000)
#        ** (EXIT) time out
#        (elixir 1.11.3) lib/task.ex:725: Task.await_many/5
#        (elixir 1.11.3) lib/task.ex:709: Task.await_many/2
透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 任務

練習已鎖定

再解鎖 1 個練習,就能練習 任務