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 一次。
start/start_link
如果你只想為了副作用而啟動 Task,請使用 Task.start/1 或 Task.start_link/1。Task.start/1 會啟動一個未連結到呼叫端行程的 Task,而 Task.start_link/1 則會啟動一個連結到呼叫端行程的 Task。這兩個函式都會回傳一個 {:ok, pid} 元組。
常見的模式是先啟動非同步的 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/3Task.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/2Task.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