Связи и мониторы: 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.
Когда что выбирать
| Свойство | link | monitor |
| Направление | двустороннее | одностороннее |
| По умолчанию | убивает связанного | шлёт сообщение |
| Применение | группы «жить-умирать вместе», супервизоры | временное наблюдение, запрос-ответ |
Практическое правило: если процессы — части одного целого и должны жить и умирать вместе, берите 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.