Os processos do Elixir são isolados e, por padrão, não compartilham nada. Quando um processo filho não vinculado falha, o processo pai não é afetado.
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
Esse comportamento pode ser alterado ao vincular processos uns aos outros. Se dois processos estão vinculados, uma falha em um deles será propagada para o outro. Os vínculos são bidirecionais.
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
Um processo pode ser criado já vinculado ao processo que fez a chamada usando spawn_link/1, que é uma operação atômica, ou pode ser vinculado depois com Process.link/1.
Você pode verificar quais processos estão vinculados a um processo no momento (e ainda em execução) com 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>]
Vincular processos pode ser útil em trabalho paralelizado, quando cada parte do trabalho não deve continuar caso outra parte não consiga terminar.
Vincular processos também pode ser usado para supervisionar processos. Se um processo captura exits, ele não vai falhar quando um processo ao qual está vinculado falhar. Em vez disso, ele vai receber uma mensagem sobre a falha. Isso permite que ele lide com a falha de forma tranquila, por exemplo, reiniciando o processo que falhou.
Um processo pode ser configurado para capturar exits chamando Process.flag(:trap_exit, true). Repare que Process.flag/2 retorna o valor antigo da flag, não o novo.
Você pode verificar o valor atual da flag usando Process.info/1.
pid
|> Process.info()
|> Keyword.get(:trap_exit)
# => false
A mensagem que será enviada ao processo caso um processo vinculado falhe vai corresponder ao padrão {:EXIT, from, reason}, em que from é um PID. Se reason for qualquer coisa diferente do átomo :normal, isso significa que o processo falhou ou foi encerrado à força.
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
Repare que capturar exits torna o processo imune a falhas em todos os seus processos vinculados, e as mensagens de exit podem se acumular na caixa de mensagens do processo se não forem lidas. Por isso, você deve acompanhar todos os processos vinculados a um processo que captura exits. Normalmente, processos que capturam exits são responsáveis apenas por supervisionar outros processos e não fazem mais nada.
Na prática, você não implementaria um processo supervisor do zero. O Elixir oferece uma abstração para esse tipo de funcionalidade na forma do comportamento Supervisor.
Outras abstrações em torno de processos, como Agent, GenServer ou Task, oferecem duas formas de iniciá-las: start ou start_link.