Les processus Elixir sont isolés et ne partagent rien par défaut. Quand un processus enfant non lié plante, son processus parent n'est pas affecté.
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
Ce comportement peut être modifié en liant les processus entre eux. Si deux processus sont liés, une défaillance dans l'un sera propagée à l'autre. Les liens sont bidirectionnels.
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
On peut lancer des processus déjà liés au processus appelant avec spawn_link/1, ce qui est une opération atomique, ou bien les lier plus tard avec Process.link/1.
On peut vérifier quels processus sont actuellement liés à un processus (et toujours en cours d'exécution) avec 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>]
Lier des processus peut être utile pour du travail parallélisé, quand chaque morceau de travail ne doit pas être poursuivi si un autre morceau n'arrive pas à se terminer.
Le lien peut aussi servir à superviser des processus. Si un processus intercepte les sorties, il ne plantera pas quand un processus auquel il est lié plante. Il recevra à la place un message concernant le plantage. Cela lui permet de gérer le plantage proprement, par exemple en redémarrant le processus qui a planté.
On peut configurer un processus pour qu'il intercepte les sorties en appelant Process.flag(:trap_exit, true). À noter que Process.flag/2 renvoie l'ancienne valeur du drapeau, pas la nouvelle.
On peut vérifier la valeur actuelle du drapeau avec Process.info/1.
pid
|> Process.info()
|> Keyword.get(:trap_exit)
# => false
Le message qui sera envoyé au processus si un processus lié plante correspondra au motif {:EXIT, from, reason}, où from est un PID. Si reason est autre chose que l'atome :normal, cela signifie que le processus a planté ou a été tué de force.
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
À noter qu'intercepter les sorties rend le processus insensible aux plantages de tous ses processus liés, et que les messages de sortie peuvent s'accumuler dans la boîte aux lettres du processus s'ils ne sont pas lus. Pour cette raison, il faut garder la trace de tous les processus liés à un processus qui intercepte les sorties. En général, les processus qui interceptent les sorties ne font que superviser d'autres processus et ne font rien d'autre.
En pratique, tu n'implémenterais pas toi-même un processus superviseur de zéro. Elixir fournit une abstraction pour ce genre de fonctionnalité sous la forme du comportement Supervisor.
D'autres abstractions autour des processus, comme Agent, GenServer ou Task, proposent deux façons de les démarrer : soit start, soit start_link.