Ta

Task in Elixir

2 esercizi

Informazioni su Task

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.

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}.

Timeout

È 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/3

Task.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/2

Task.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
Modifica tramite GitHub Il collegamento si apre in una nuova finestra o scheda

Impara Task

La pratica è bloccata

Sblocca un altro esercizio per esercitarti su Task