Зв

Звʼязки у Elixir

1 вправа

Про концепцію Звʼязки

Процеси в 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.

Редагувати через GitHub Посилання відкривається в новому вікні або вкладці

Вивчити концепцію Звʼязки