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.
parent_pid =
spawn(fn ->
spawn(fn -> raise "oops" end)
receive do
message -> IO.inspect(message, label: "received message")
end
end)
# => 20:03:08.405 [error] Process #PID<0.153.0> raised an exception
# ** (RuntimeError) oops
# (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6
Process.alive?(parent_pid)
# => true
Questo comportamento può essere modificato collegando i processi tra loro. Se due processi sono collegati, un errore in uno dei due si propaga all'altro processo. I collegamenti sono bidirezionali.
parent_pid =
spawn(fn ->
spawn_link(fn -> raise "oops" end)
receive do
message ->
IO.inspect(message, label: "received message")
end
end)
# => 20:05:34.125 [error] Process #PID<0.171.0> raised an exception
# ** (RuntimeError) oops
# (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6
Process.alive?(parent_pid)
# => false
I processi possono essere generati già collegati al processo chiamante usando spawn_link/1, che è un'operazione atomica, oppure possono essere collegati in seguito con Process.link/1.
Puoi verificare quali processi sono attualmente collegati ad un processo (ed ancora in esecuzione) con Process.info/1.
pid = spawn_link(fn -> :timer.sleep(50_000) end)
# => #PID<0.126.0>
self()
|> Process.info()
|> Keyword.get(:links)
# => [#PID<0.126.0>]
Collegare i processi può essere utile quando si svolge un lavoro parallelizzato, in cui ogni porzione di lavoro non dovrebbe andare avanti se un'altra porzione non riesce a terminare.
Il collegamento può essere usato anche per supervisionare i processi. Se un processo intercetta le uscite, non va in crash quando va in crash un processo al quale è collegato. Riceve invece un messaggio che segnala il 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 intercettare le uscite chiamando Process.flag(:trap_exit, true). Nota che Process.flag/2 restituisce il valore precedente del flag, non quello nuovo.
Il valore attuale del flag si può verificare con Process.info/1.
pid
|> Process.info()
|> Keyword.get(:trap_exit)
# => false
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 o è stato terminato forzatamente.
parent_pid =
spawn(fn ->
Process.flag(:trap_exit, true)
spawn_link(fn -> raise "oops" end)
receive do
message ->
IO.inspect(message, label: "received message")
# do something else,
# to demonstrate that the process stays alive
:timer.sleep(50_000)
end
end)
# => 20:05:34.125 [error] Process #PID<0.295.0> raised an exception
# ** (RuntimeError) oops
# (stdlib 3.13.2) erl_eval.erl:678: :erl_eval.do_apply/6
# received message: {:EXIT, #PID<0.295.0>,
# {%RuntimeError{message: "oops"},
# [{:erl_eval, :do_apply, 6, [file: 'erl_eval.erl', line: 678]}]}}
Process.alive?(parent_pid)
# => true
Nota che intercettare le uscite rende il processo immune ai crash di tutti i processi ad esso collegati, ed i messaggi di uscita potrebbero accumularsi nella casella di posta del processo se non vengono letti. Per questo motivo, dovresti tenere traccia di tutti i processi collegati ad un processo che intercetta le uscite. Di solito, i processi che intercettano le uscite si occupano solo di supervisionare altri processi e non fanno nient'altro.
In pratica, non dovrai implementare da zero un processo supervisore. Elixir fornisce un'astrazione per questo tipo di funzionalità sotto forma del comportamento Supervisor.
Altre astrazioni basate sui processi, come Agent, GenServer o Task, offrono due modi per avviarli: start oppure start_link.