Состояние в процессах, связи и мониторинг

У процессов нет переменных, которые «живут между вызовами». Состояние держат в аргументах цикла. А связи и мониторы превращают падения процессов в управляемые события.

Состояние процесса — это его «память». Передавайте его параметром цикла, обновляя при каждой итерации. А links и monitors — нервная система отказоустойчивости.

Чтобы процесс хранил состояние, он передаёт его в рекурсивный loop и обновляет на каждом сообщении:

defmodule Counter do
  def start(initial), do: spawn(fn -> loop(initial) end)

  defp loop(count) do
    receive do
      :inc -> loop(count + 1)
      {:get, from} ->
        send(from, {:count, count})
        loop(count)
    end
  end
end

Связь (spawn_link) делает два процесса «связанными»: падение одного посылает сигнал выхода другому. Монитор — односторонний: вы получаете сообщение, когда наблюдаемый процесс упал, но сами не падаете.

# Монитор: узнаём о падении, не падая сами
{pid, ref} = spawn_monitor(fn -> raise "boom" end)

receive do
  {:DOWN, ^ref, :process, ^pid, reason} ->
    IO.puts("Процесс упал: #{inspect(reason)}")
end

Как работает под капотом (BEAM)

Состояние «между сообщениями» хранится не в переменной, а в аргументе хвостового цикла: каждая итерация получает свежее неизменяемое значение. Связи (links) двунаправлены: если связанный процесс падает, по связи бежит сигнал выхода, и по умолчанию получатель тоже падает — так сбой «всплывает» к супервизору. Мониторы односторонни и не валят наблюдателя: вместо этого он получает сообщение {:DOWN, ref, ...}. Эти примитивы — фундамент, на котором супервизоры строят автоматический перезапуск: именно через links падение ребёнка доходит до супервизора.

  Связь (link), двусторонняя:
    [A] === [B]    падение A  =>  сигнал выхода в B (B тоже падает)

  Монитор (monitor), односторонний:
    [Наблюдатель] ...мониторит...> [B]
    падение B  =>  {:DOWN, ...} в почту наблюдателя (он жив)

Та же идея на Python ▶

Состояние в цикле и реакцию на «падение» подчинённого смоделируем явно.

# Состояние живёт в параметре "цикла", а не в глобальной переменной
def counter_loop(messages, count=0):
    for msg in messages:
        match msg:
            case "inc":           count += 1
            case ("get", box):    box.append(count)   # "ответ" наблюдателю
    return count

box = []
final = counter_loop(["inc", "inc", ("get", box), "inc"])
print(box)      # [2]  — состояние на момент get
print(final)    # 3

# "Монитор": ловим падение подчинённого, сами не падаем
def supervise(task):
    try:
        task()
    except Exception as e:
        return ("DOWN", str(e))    # как сообщение {:DOWN, ...}
    return ("UP", None)

print(supervise(lambda: (_ for _ in ()).throw(RuntimeError("boom"))))
# ('DOWN', 'boom')

Частые ошибки

  • Хранить состояние «снаружи». Состояние — в аргументе цикла; глобальных мутируемых переменных нет.
  • Путать link и monitor. Link валит вас при падении партнёра; monitor лишь уведомляет.
  • Ставить голый link без супервизии. Связь без стратегии перезапуска просто «утянет» вас за собой.

Best practices

  • Передавайте состояние параметром хвостового цикла и обновляйте на каждом сообщении.
  • Используйте monitor, когда нужно лишь узнать о падении; link — когда судьбы процессов должны быть связаны.
  • Не городите links вручную для надёжности — это работа супервизоров OTP.

Итог. Состояние в цикле, links и monitors — это сырые примитивы, на которых стоит вся отказоустойчивость BEAM. Писать их руками утомительно, поэтому OTP даёт готовые абстракции. К ним — в финальном разделе.

От ручных примитивов к OTP

Этот урок показывает «сырьё», из которого сделана отказоустойчивость: цикл с состоянием в аргументе, links для распространения сбоев, monitors для уведомлений. Понимать их важно, но писать руками каждый раз — нет. Почти всё, что мы здесь собрали вручную, в реальном коде делают абстракции OTP: GenServer ведёт цикл и хранит состояние за вас, супервизор расставляет links и реализует стратегию перезапуска, мониторы прячутся внутри библиотечных функций.

Тем не менее эти знания не пропадут зря. Когда вы будете отлаживать, почему «упал один процесс, а за ним посыпались соседние», вы вспомните про links и распространение сигналов выхода. Когда понадобится узнать о завершении задачи, не связывая с ней свою судьбу, вы возьмёте monitor. OTP не отменяет эти примитивы — он строится на них и иногда требует спуститься на их уровень. Так что считайте этот урок взглядом «под капот» того, что в следующем разделе станет одной строкой use GenServer.

Проверьте себя
1. Где процесс хранит состояние между сообщениями?
AВ глобальной переменной
BВ аргументе своего рекурсивного (хвостового) цикла, обновляя его на каждой итерации
CНа диске
DВ mailbox
2. Чем монитор (monitor) отличается от связи (link)?
AНичем
BМонитор односторонний и лишь уведомляет {:DOWN, ...} о падении, не роняя наблюдателя; link двусторонний и распространяет падение
CМонитор быстрее
DLink работает только в Erlang