Процеси в Elixir ізольовані й типово нічого не поділяють між собою. Коли незвʼязаний дочірній процес аварійно завершується, це не впливає на батьківський процес.
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
Таку поведінку можна змінити, звʼязавши процеси один з одним. Якщо два процеси звʼязані, збій в одному з них поширюється на інший. Звʼязки двонапрямні.
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
Процес можна створити вже звʼязаним із процесом, що його викликає, за допомогою spawn_link/1 (це атомарна операція), або звʼязати його пізніше за допомогою Process.link/1.
Перевірити, які процеси наразі звʼязані з процесом (і досі працюють), можна за допомогою 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>]
Звʼязування процесів стає в пригоді в паралельній роботі, коли не варто продовжувати кожен фрагмент, якщо інший фрагмент не може завершитися.
Звʼязування також можна використати для нагляду за процесами. Якщо процес перехоплює виходи, він не аварійно завершиться, коли аварійно завершиться звʼязаний із ним процес. Натомість він отримає повідомлення про цю аварію. Це дає йому змогу коректно обробити аварію, наприклад перезапустивши процес, що завершився аварійно.
Процес можна налаштувати так, щоб він перехоплював виходи, викликавши Process.flag(:trap_exit, true). Зауважмо, що Process.flag/2 повертає старе значення прапорця, а не нове.
Поточне значення прапорця можна перевірити за допомогою Process.info/1.
pid
|> Process.info()
|> Keyword.get(:trap_exit)
# => false
Повідомлення, яке отримає процес, якщо звʼязаний із ним процес аварійно завершиться, відповідатиме зразку {:EXIT, from, reason}, де from позначає PID. Якщо reason не дорівнює атому :normal, це означає, що процес аварійно завершився або його примусово вбили.
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
Зауважмо, що перехоплення виходів робить процес нечутливим до аварій у всіх звʼязаних із ним процесах, а повідомлення про вихід можуть накопичуватися в поштовій скриньці процесу, якщо їх не читати. Тому варто стежити за всіма процесами, звʼязаними з процесом, який перехоплює виходи. Зазвичай процеси, що перехоплюють виходи, відповідають лише за нагляд за іншими процесами й більше нічого не роблять.
На практиці ми не стали б писати процес-наглядач з нуля власноруч. Elixir надає абстракцію для такої функціональності у вигляді поведінки Supervisor.
Інші абстракції для роботи з процесами, як-от Agent, GenServer чи Task, дають два способи їх запуску: start або start_link.