Связи и мониторы: link и monitor

Как процессы узнают о смерти друг друга.

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

Изолированные процессы — это хорошо, но иногда нужно знать, что другой процесс упал, чтобы среагировать. Память процессов раздельна, и сам по себе сбой одного никак не виден соседям: упавший процесс просто исчезает, а остальные продолжают работать, словно ничего не случилось. Это и есть изоляция, ради которой всё затевалось. Но в реальной системе процессы не живут в вакууме: один обслуживает соединение, другой держит кэш, третий координирует работу. Если рабочий умер, кто-то должен это заметить и принять решение — перезапустить, отменить транзакцию, освободить ресурс. Erlang даёт для этого два механизма обнаружения отказов: связи и мониторы. Они кажутся похожими, но решают разные задачи, и путать их — частый источник ошибок у новичков. На этих двух примитивах построены супервизоры, а значит и вся отказоустойчивость OTP, так что понять их разницу стоит основательно.

Связи (link)

Когда два процесса связаны и один падает, второму приходит сигнал выхода. По умолчанию этот сигнал убивает и второй процесс — отказ распространяется по связям. Это нужно, чтобы группа взаимозависимых процессов «жила и умирала вместе». Представьте бригаду, где люди связаны одной верёвкой: если один срывается в пропасть, без специальных мер он утянет за собой остальных. На первый взгляд это звучит пугающе — зачем нам, чтобы падение одного убивало других? Но смысл глубокий: если процессы образуют единое целое и один из них больше не может выполнять свою часть работы, то и остальные часто оказываются в неполноценном, противоречивом состоянии. Лучше честно завершить всю группу и перезапустить её с чистого листа, чем тащить дальше наполовину сломанную систему. Связь по умолчанию симметрична и «заразна»: неважно, кто из двоих упал первым, сигнал пойдёт в обе стороны.

Pid = spawn_link(fun() ->
    timer:sleep(1000),
    exit(boom)   % процесс падает
end).
% Так как мы связаны (spawn_link), сигнал boom
% по умолчанию завершит и наш процесс.

spawn_link — это spawn плюс мгновенная связь, атомарно. Это важно: связать после spawn отдельно — значит оставить окно, в котором процесс может упасть незамеченным. Допустим, вы написали Pid = spawn(Fun), а затем link(Pid) отдельной строкой. Между этими двумя действиями проходит крошечный, но ненулевой промежуток времени. Если новый процесс успеет упасть именно в этот момент — например, на самой первой строке функции, — то ваша связь установится уже на мёртвый процесс, и вы либо получите сигнал немедленно, либо (что хуже в некоторых сценариях) пропустите момент гибели. Атомарность spawn_link закрывает это окно гонки: связь существует с самого первого мгновения жизни ребёнка. По той же причине у мониторов есть атомарный аналог — spawn_monitor. Запомните общее правило: всё, что касается времени жизни процесса, лучше делать одной операцией, а не двумя по отдельности.

Перехват сигналов: trap_exit

Иногда нужно не умереть вместе со связанным процессом, а узнать о его смерти и обработать. Для этого процесс ставит флаг trap_exit. Тогда сигнал выхода превращается в обычное сообщение {'EXIT', Pid, Reason}, которое можно поймать в receive. По сути флаг переключает процесс из режима «умри вместе со связанным» в режим «получи уведомление и реши сам, что делать». Это превращает обычный процесс в потенциального наблюдателя за группой. Важная тонкость: есть один особый случай — сигнал с причиной kill. Это «безусловное убийство», которое не превращается в сообщение даже при включённом trap_exit и завершает процесс в любом случае. Так сделано намеренно: должен существовать способ гарантированно остановить процесс, который иначе перехватил бы и проигнорировал все попытки его завершить. Поэтому trap_exit — мощный инструмент, но не абсолютная броня.

process_flag(trap_exit, true),
spawn_link(fun() -> exit(crashed) end),
receive
    {'EXIT', From, Reason} ->
        io:format("Процесс ~p упал: ~p~n", [From, Reason])
end.

Именно так работают супервизоры: они перехватывают сигналы выхода своих детей и решают, что делать. Супервизор — это процесс с включённым trap_exit, связанный со всеми своими подопечными; когда любой из них умирает, супервизор получает сообщение {'EXIT', ...} и по заранее заданной стратегии решает, перезапустить ли упавшего, остановить ли соседей или сдаться и эскалировать проблему выше. Без trap_exit ничего этого не вышло бы: супервизор просто умирал бы вместе с первым же упавшим ребёнком. Так что этот неприметный флаг — буквально та шестерёнка, на которой держится вся машина восстановления OTP.

Мониторы (monitor)

Связь двунаправленная и «заразная». Когда нужно просто понаблюдать за процессом, не связывая судьбы, используют монитор — одностороннее наблюдение. О смерти наблюдаемого приходит сообщение {'DOWN', Ref, process, Pid, Reason}, и оно не убивает наблюдателя. Аналогия проста: связь — это как быть привязанным к напарнику одной верёвкой, а монитор — как поставить камеру наблюдения. Камера сообщит вам, что за объектом что-то случилось, но сама от этого не пострадает. Каждый монитор уникален: при его установке возвращается ссылка Ref, которая попадает и в сообщение 'DOWN'. Благодаря этому вы можете поставить несколько мониторов на один и тот же процесс и потом точно понять, какой именно из них сработал. Мониторы незаменимы в запрос-ответных сценариях: вы спрашиваете другой процесс и хотите не зависнуть навечно, если он умрёт, не успев ответить, — поставленный на время монитор как раз и даёт вам уведомление о такой смерти.

Ref = monitor(process, Pid),
receive
    {'DOWN', Ref, process, Pid, Reason} ->
        io:format("Наблюдаемый умер: ~p~n", [Reason])
end.

Когда что выбирать

Свойствоlinkmonitor
Направлениедвустороннееодностороннее
По умолчаниюубивает связанногошлёт сообщение
Применениегруппы «жить-умирать вместе», супервизорывременное наблюдение, запрос-ответ

Практическое правило: если процессы — части одного целого и должны жить и умирать вместе, берите link (а на деле — супервизор, который делает это правильно). Если же вы лишь временный наблюдатель и не хотите, чтобы чужая смерть утянула вас за собой, берите monitor. Симметрия — главное различие: связь действует в обе стороны и заразна, монитор строго односторонен и безопасен для наблюдателя.

Снятие связей и мониторов

Оба механизма можно отменить. Связь снимается вызовом unlink, монитор — вызовом demonitor. Это нужно, когда наблюдение было временным: например, вы поставили монитор на время одного запроса и, получив ответ, больше не хотите узнавать о судьбе того процесса. Здесь кроется тонкая гонка: процесс мог умереть ровно в тот миг, когда вы вызываете demonitor, и сообщение 'DOWN' уже летит к вам в почтовый ящик. Чтобы не оставлять такие «осиротевшие» сообщения, используют форму demonitor(Ref, [flush]) — она и снимает монитор, и вычищает уже пришедшее уведомление, если оно есть. Привыкайте думать о таких пограничных моментах: в конкурентной системе «почти одновременно» — это нормальное состояние, и аккуратная работа с ним отличает надёжный код от хрупкого.

Как работает под капотом

Связи и мониторы хранятся в структуре процесса. При завершении процесса BEAM проходит по его связям и мониторам, рассылая сигналы и сообщения. Сигнал выхода с причиной normal (нормальное завершение) по связи никого не убивает — гибнут только связанные при «ненормальной» причине. Это важная деталь: процесс, который штатно доделал работу и завершился с normal, не тащит за собой связанных, а вот тот, что упал с ошибкой вроде badmatch или boom, распространит сигнал по всем своим связям. Монитор автоматически снимается после прихода 'DOWN', так что повторных сообщений не будет — это удобно, не нужно вручную чистить за собой одноразовое наблюдение. Сами сигналы — это не обычные сообщения почтового ящика, а отдельный, более низкоуровневый механизм рантайма, который и обеспечивает мгновенность распространения отказа по графу процессов.

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

  • Использовать spawn + link по отдельности. Берите spawn_link — атомарно, без окна гонки.
  • Ждать, что монитор убьёт наблюдателя. Монитор только уведомляет, не распространяет смерть.
  • Забыть trap_exit там, где нужно пережить смерть ребёнка. Иначе умрёте вместе с ним.

Итоги

  • link — двусторонняя связь; смерть одного по умолчанию убивает связанного.
  • trap_exit превращает сигнал выхода в сообщение {'EXIT', ...}.
  • monitor — одностороннее наблюдение через сообщение {'DOWN', ...}.
  • Эти механизмы — основа супервизоров и устойчивости Erlang.
Проверьте себя
1. Чем link отличается от monitor?
AНичем
Blink — двусторонняя связь, по умолчанию убивает связанного; monitor — одностороннее наблюдение через сообщение
Cmonitor убивает наблюдателя
Dlink только уведомляет
2. Что делает флаг trap_exit?
AОтключает процесс
BПревращает сигнал выхода связанного процесса в обычное сообщение {'EXIT', ...}
CЗапрещает падения
DУскоряет приём
3. Почему предпочитают spawn_link вместо spawn и затем link?
Aspawn_link быстрее печатает
BОн связывает атомарно, без окна, где процесс может упасть незамеченным
Clink не существует
DТак требует синтаксис