I processi di Elixir sono isolati e per impostazione predefinita non condividono nulla. Quando un processo figlio non collegato va in crash, il processo padre non ne risente.
Questo comportamento si può cambiare collegando i processi tra loro. Se due processi sono collegati, un errore in uno dei due si propaga all'altro. I collegamenti sono bidirezionali.
I processi possono essere avviati già collegati al processo chiamante con spawn_link/1, che è un'operazione atomica, oppure possono essere collegati in un secondo momento con Process.link/1.
Collegare i processi può essere utile quando si svolge un lavoro in parallelo: ogni porzione di lavoro non dovrebbe proseguire se un'altra porzione non riesce a concludersi.
Il collegamento può essere usato anche per supervisionare i processi. Se un processo cattura le uscite, non va in crash quando un processo a cui è collegato va in crash. Riceverà invece un messaggio relativo al crash. Questo gli permette di gestire il crash in modo controllato, per esempio riavviando il processo andato in crash.
Un processo può essere configurato per catturare le uscite chiamando Process.flag(:trap_exit, true). Nota che Process.flag/2 restituisce il valore precedente del flag, non quello nuovo.
Il messaggio che viene inviato al processo quando un processo collegato va in crash corrisponde al pattern {:EXIT, from, reason}, dove from è un PID. Se reason è qualcosa di diverso dall'atomo :normal, significa che il processo è andato in crash oppure è stato terminato forzatamente.
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 usati comunemente per parallelizzare il lavoro.
async/await
Per avviare un task, usa Task.async/1. Prende come argomento una funzione anonima e la esegue in un nuovo processo collegato al processo chiamante. Restituisce una struct %Task{}.
Per ottenere il risultato dell'esecuzione, passa la struct %Task{} a Task.await/2. Aspetterà che il task finisca e ne restituirà il risultato. Il secondo argomento è un timeout in millisecondi, con valore predefinito 5000.
Nota che tra l'avvio del task e l'attesa del task, il processo che ha avviato il task non è 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 una sola volta per ogni task.
start/start_link
Se vuoi avviare un task solo per i suoi effetti collaterali, usa Task.start/1 o Task.start_link/1. Task.start/1 avvia un task non collegato al processo chiamante, mentre Task.start_link/1 avvia un task collegato al processo chiamante. Entrambe le funzioni restituiscono una tupla {:ok, pid}.
Il tuo lavoro presso Instruments of Texas su una calcolatrice RPN sperimentale continua. Il tuo team ha costruito alcuni prototipi che devono essere sottoposti a un'ispezione approfondita, per scegliere la migliore da produrre in serie.
Vuoi eseguire due tipi di controlli.
Innanzitutto, un controllo di affidabilità che rileverà gli input per i quali la calcolatrice in esame va in crash o non risponde abbastanza velocemente. Per isolare i guasti, i calcoli per ogni input devono essere eseguiti in un processo separato. Collegare i processi e intercettare le uscite nel processo chiamante permette di rilevare se il calcolo è terminato o è andato in crash.
In secondo luogo, un controllo di correttezza che verificherà se, per un dato input, il risultato restituito dalla calcolatrice è quello atteso. Solo le calcolatrici che hanno già superato il controllo di affidabilità verranno sottoposte a un controllo di correttezza, quindi i crash non sono un problema. Tuttavia, le operazioni dovrebbero essere eseguite in modo concorrente per velocizzare il processo: è il caso d'uso perfetto per i task asincroni.
Implementa la funzione RPNCalculatorInspection.start_reliability_check/2. Dovrebbe prendere 2 argomenti: una funzione (la calcolatrice) e un input per la calcolatrice. Dovrebbe restituire una mappa che contiene l'input e il PID del processo generato.
Il processo generato deve chiamare la funzione calcolatrice passata con l'input dato. Il processo deve essere collegato al processo chiamante.
RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}
Implementa la funzione RPNCalculatorInspection.await_reliability_check_result/2. Dovrebbe prendere due argomenti. Il primo argomento è una mappa con l'input del controllo di affidabilità e il PID del processo che esegue il controllo di affidabilità per questo input, come restituito da RPNCalculatorInspection.start_reliability_check/2. Il secondo argomento è una mappa che funge da accumulatore per i risultati dei controlli di affidabilità con input diversi.
La funzione deve attendere un messaggio di uscita.
Se riceve un messaggio di uscita ({:EXIT, from, reason}) con il motivo :normal dallo stesso processo che esegue il controllo di affidabilità, deve restituire la mappa dei risultati con il valore :ok aggiunto sotto la chiave input.
Se riceve un messaggio di uscita con un motivo diverso dallo stesso processo che esegue il controllo di affidabilità, deve restituire la mappa dei risultati con il valore :error aggiunto sotto la chiave input.
Se non riceve alcun messaggio che soddisfi questi criteri entro 100ms, deve restituire la mappa dei risultati con il valore :timeout aggiunto sotto la chiave input.
# when an exit message is waiting for the process in its inbox
send(self(), {:EXIT, pid, :normal})
RPNCalculatorInspection.await_reliability_check_result(
%{input: "5 7 -", pid: pid},
%{}
)
# => %{"5 7 -" => :ok}
# when there are no messages in the process inbox
RPNCalculatorInspection.await_reliability_check_result(
%{input: "3 2 *", pid: pid},
%{"5 7 -" => :ok}
)
# => %{"5 7 -" => :ok, "3 2 *" => :timeout}
Implementa la funzione RPNCalculatorInspection.reliability_check/2. Dovrebbe prendere 2 argomenti: una funzione (la calcolatrice) e un array di input per la calcolatrice.
Per ogni input dell'array, deve avviare il controllo di affidabilità in un nuovo processo collegato usando start_reliability_check/2. Poi, per ogni processo avviato in questo modo, deve attendere i suoi risultati usando await_reliability_check_result/2.
Prima di avviare qualsiasi processo, la funzione deve impostare il flag sul processo corrente per intercettare le uscite, così da poter ricevere i messaggi di uscita. Successivamente, deve ripristinare questo flag al suo valore originale.
La funzione deve restituire una mappa con i risultati dei controlli di affidabilità di tutti gli input.
fake_broken_calculator = fn input ->
if String.ends_with?(input, "*"), do: raise("oops")
end
inputs = ["2 3 +", "10 3 *", "20 2 /"]
RPNCalculatorInspection.reliability_check(fake_broken_calculator, inputs)
# => %{
# "2 3 +" => :ok,
# "10 3 *" => :error,
# "20 2 /" => :ok
# }
Implementa la funzione RPNCalculatorInspection.correctness_check/2. Dovrebbe prendere 2 argomenti: una funzione (la calcolatrice) e un array di input per la calcolatrice.
Per ogni input dell'array, deve avviare un task asincrono che chiamerà la calcolatrice con l'input dato. Poi, per ogni task avviato in questo modo, deve attendere i suoi risultati per 100ms.
fast_cheating_calculator = fn input -> 14 end
inputs = ["13 1 +", "50 2 *", "1000 2 /"]
RPNCalculatorInspection.correctness_check(fast_cheating_calculator, inputs)
# => [14, 14, 14]
Iscriviti a Exercism per imparare e padroneggiare Elixir con 58 concetti168 esercizi e il mentoring di persone reali, tutto gratis.