I task sono processi pensati per eseguire una specifica operazione. Di solito non comunicano con altri processi, ma possono restituire un risultato al processo che ha avviato il task.
I task sono comunemente usati per parallelizzare il lavoro.
async/await
Per avviare un task, usa Task.async/1. Prende una funzione anonima come argomento e la esegue in un nuovo processo collegato al processo chiamante. Restituisce una struttura %Task{}.
Per ottenere il risultato dell'esecuzione, passa la struttura %Task{} a Task.await/2. Attenderà che il task finisca e restituirà il suo risultato. Il secondo argomento è un timeout in millisecondi, che per impostazione predefinita è 5000. Il valore :infinity può essere passato come timeout per permettere al task di essere eseguito senza limiti di tempo.
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
Nota che tra l'avvio del task e l'attesa del task, il processo che ha avviato il task non viene bloccato e può svolgere altre operazioni.
Ogni task avviato con Task.async/1 dovrebbe essere atteso, perché invierà un messaggio al processo chiamante. Task.await/2 può essere chiamato per ogni task una sola volta.
start/start_link
Se vuoi avviare un task solo per i suoi effetti collaterali, usa Task.start/1 oppure Task.start_link/1. Task.start/1 avvierà un task non collegato al processo chiamante, mentre Task.start_link/1 avvierà un task collegato al processo chiamante. Entrambe le funzioni restituiscono una tupla {:ok, pid}.
È uno schema comune avviare task asincroni e poi attenderli usando Enum.map/2, in questo modo:
inputs
|> Enum.map(&Task.async(fn -> function.(&1) end))
|> Enum.map(&Task.await(&1, 1_000))
È importante essere consapevoli che Task.await è sincrono e che, ogni volta che viene chiamato, fa ripartire da 0 il conteggio del suo timeout. Questo significa che, quando viene usato in questo modo, in sequenza, i timeout si sommano.
Il primo task che viene atteso non verrà eseguito più a lungo del timeout, ma il secondo task che viene atteso potrà essere eseguito per tutto il tempo in cui è stato eseguito il primo task, più il timeout.
Considera una situazione in cui ogni task impiega un po' più tempo a essere eseguito rispetto al precedente, ma la differenza nella durata di esecuzione dei task è inferiore al timeout di attesa.
inputs = 1..10
function = fn n -> :timer.sleep(n * 900) && n end
Possiamo usare :timer.tc/1, una funzione Erlang, per misurare quanto tempo impiegheranno quei task a essere eseguiti, in microsecondi.
: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 microsecondi sono 9 secondi, molto più del timeout indicato di 1 secondo (1000 millisecondi).
Se questo è il comportamento che vuoi, benissimo, vai pure! In caso contrario, l'alternativa è usare Task.async_stream/3 oppure Task.await_many/2.
Task.async_stream/3Task.async_stream/3 ti permette di eseguire la stessa funzione su molti input diversi, ciascuno nel proprio task. I task sono collegati al processo corrente, proprio come quando vengono eseguiti con Task.async/1. Restituisce uno stream.
Accetta un timeout che si applica a ogni task separatamente. Considera gli stessi input e la stessa funzione usati con 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 è ottimo se hai un'unica funzione da eseguire con input diversi. Offre molto controllo, perché accetta altre opzioni utili, come quanti task eseguire contemporaneamente e cosa fare in caso di timeout di un task.
Se devi eseguire task che svolgono operazioni diverse, potresti aver bisogno invece di Task.await_many/2.
Task.await_many/2Task.await_many/2 è stato introdotto in Elixir 1.11. Ti permette di attendere molti task con un timeout condiviso. Considera gli stessi input e la stessa funzione usati con 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