Плохо ли отправлять сообщение self() в init?

В этом примере автор избегает тупиковой ситуации, делая следующее:

self() ! {start_worker_supervisor, Sup, MFA}

в функции инициализации его gen_server. Я сделал что-то подобное в одном из своих проектов, и мне сказали, что этот метод не одобряется и что вместо этого лучше вызвать немедленный тайм-аут. Какова принятая схема?


person David Weldon    schedule 19.05.2011    source источник


Ответы (5)


Обновление для Эрланга 19+

Рассмотрите возможность использования нового поведения gen_statem. Это поведение поддерживает генерацию событий внутри FSM:

Функция состояния может вставлять события с помощью action() next_event, и такое событие вставляется как следующее для представления функции состояния. То есть, как будто это самое старое входящее событие. Для таких событий можно использовать специальный внутренний event_type(), что делает невозможным их ошибочное принятие за внешние события.

Вставка события заменяет прием вызова ваших собственных функций обработки состояния, к которым вам часто приходилось бы прибегать, например, в gen_fsm, чтобы принудительно обработать вставленное событие раньше других.

Используя функционал действия в этом модуле, вы можете убедиться, что ваше событие сгенерировано в init и всегда обрабатывается перед любыми внешними событиями, в частности, путем создания действия next_event в вашей функции init.

Пример:

...

callback_mode() -> state_functions.

init(_Args) ->
    {ok, my_state, #data{}, [{next_event, internal, do_the_thing}]}

my_state(internal, do_the_thing, Data) ->
    the_thing(),
    {keep_state, Data);
my_state({call, From}, Call, Data) ->
    ...

...

Старый ответ

При разработке gen_server у вас обычно есть возможность выполнять действия в трех разных состояниях:

  • При запуске в init/1
  • При запуске в любой handle_* функции
  • При остановке в terminate/2

Хорошее эмпирическое правило — выполнять действия в функциях обработки при воздействии на событие (вызов, приведение, сообщение и т. д.). Вещи, которые выполняются в init, не должны ждать событий, для чего нужны обратные вызовы дескрипторов.

Итак, в данном конкретном случае генерируется своего рода поддельное событие. Я бы сказал, что gen_server всегда хочет инициировать запуск супервизора. Почему бы просто не сделать это прямо в init/1? Действительно ли требуется иметь возможность обрабатывать другое сообщение между ними (вместо этого эффект выполнения в handle_info/2)? Это окно невероятно маленькое (время между запуском gen_server и отправкой сообщения self()), поэтому маловероятно, что это вообще произойдет.

Что касается тупиковой ситуации, я бы очень не советовал вызывать вашего собственного руководителя в вашей функции инициализации. Это просто плохая практика. Хорошим шаблоном проектирования для запуска рабочего процесса будет один супервайзер верхнего уровня с менеджером и рабочим супервайзером ниже. Менеджер запускает воркеров, вызывая супервайзера воркеров:

[top_sup]
  |    \
  |     \
  |      \
 man  [work_sup]
       /  |  \
      /   |   \
     /    |    \
    w1   ...   wN
person Adam Lindberg    schedule 19.05.2011
comment
Я бы не рекомендовал перемещать вещи в init/1 просто потому, что это обычно повторяющийся код. Конечно, мы могли бы извлечь его во внутреннюю функцию, но тогда код в handle_call будет менее читабелен. - person user425720; 19.05.2011
comment
Что ж, если это повторяющийся код, вы поместите его в функцию, а затем вызовете его как из init/1, так и из handle_call/3 или handle_cast/2. Я вижу, что вызовы функций намного читабельнее, чем отправка сообщений self(). - person Adam Lindberg; 20.05.2011

Просто в дополнение к тому, что уже было сказано о разделении инициализации серверов на две части: первая в функции init/1, а вторая либо в handle_cast/2, либо в handle_info/2. На самом деле есть только одна причина сделать это, и это если ожидается, что инициализация займет много времени. Затем его разделение позволит gen_server:start_link возвращаться быстрее, что может быть важно для серверов, запущенных супервизорами, поскольку они «зависают» при запуске своих дочерних элементов, а один медленный запуск дочернего элемента может задержать запуск всего супервизора.

В этом случае я не думаю, что это плохой стиль, чтобы разделить инициализацию сервера.

Важно быть осторожным с ошибками. Ошибка в init/1 приведет к завершению супервизора, в то время как ошибка во второй части заставит супервизор попытаться перезапустить этот дочерний процесс.

Лично я считаю, что для сервера лучше отправлять сообщение самому себе либо с явным !, либо с gen_server:cast, так как с хорошим описательным сообщением, например init_phase_2, будет легче увидеть, что происходит, а не более анонимный тайм-аут. Особенно, если тайм-ауты используются и в других местах.

person rvirding    schedule 19.05.2011
comment
Хороший момент о том, чтобы позволить руководителю начать контролировать процесс. Возможно, поведение должно было иметь обратный вызов second_init для начала? :-) - person Adam Lindberg; 19.05.2011

Звонить своему руководителю, конечно, кажется плохой идеей, но я постоянно делаю что-то подобное.

init(...) ->
   gen_server:cast(self(), startup),
   {ok, ...}.

handle_cast(startup, State) ->
   slow_initialisation_task_reading_from_disk_fetching_data_from_network_etc(),
   {noreply, State}.

Я думаю, что это понятнее, чем использование тайм-аута и handle_info, в значительной степени гарантируется, что ни одно сообщение не может опередить стартовое сообщение (ни у кого больше нет нашего pid до тех пор, пока мы не отправим это сообщение), и оно не попадет в способ, если мне нужно использовать тайм-ауты для чего-то еще.

person cthulahoops    schedule 19.05.2011
comment
Если ваш gen_server был запущен с gen_server:start_link/4 или gen_server:start/4, регистрация произойдет до вызова Module:init/1. Любой, кто ищет gen_server под его зарегистрированным именем, сможет найти его и отправить ему сообщение, которое придет раньше вашего собственного. - person r3m0t; 21.02.2013

Это может быть очень эффективным и простым решением, но я думаю, что это не очень хороший стиль erlang. Я использую timer:apply_after, который лучше и не производит впечатление взаимодействия с внешним модулем/gen_*.

Я думаю, что лучше всего было бы использовать конечные автоматы (gen_fsm). Большинство наших gen_srver на самом деле являются конечными автоматами, однако из-за первоначальных усилий по настройке get_fsm я думаю, что в итоге мы получим gen_srv.

В заключение я бы использовал timer:apply_after, чтобы сделать код понятным и эффективным, или gen_fsm, чтобы работать в чистом стиле Erlang (даже быстрее).

Я только что прочитал фрагменты кода, но сам пример почему-то сломан — я не понимаю эту конструкцию gen_srv, манипулирующую супервизором. Даже если он является менеджером какого-то пула будущих детей, это еще более важная причина сделать это явно, не рассчитывая на магию почтового ящика процессов. Отладка этого также была бы адом в какой-то большей системе.

person user425720    schedule 19.05.2011
comment
timer:apply_after - это всего лишь обходной путь, но реальный вопрос заключается в том, если отправка в self() в init показывает фундаментальный недостаток в дизайне. Также gen_fsm не решит эту проблему и обычно не рекомендуется вместо gen_server. - person Peer Stritzinger; 19.05.2011
comment
почему fsm не решит это? Весь смысл в том, чтобы выполнить что-то и при этом соответствовать спецификации init/1, которая, как ожидается, вернет состояние. Таким образом, мы могли бы вернуть состояние и фактически исходное состояние, выполнив предыдущую тяжелую инициализацию. - person user425720; 19.05.2011
comment
@ user4: потому что функция состояния не будет вызываться сразу, а только при вызове, приведении или тайм-ауте. Точно так же, как это делает gen_server. Вы можете запустить инициализацию с коротким (например, 0) тайм-аутом, возвращаемым из init, но вы также можете сделать это с помощью gen_server. - person Peer Stritzinger; 20.05.2011
comment
точно, но в fsm это часть четко определенного поведения - person user425720; 20.05.2011
comment
И что именно не очень понятно в gen_server по этому поводу. - person Peer Stritzinger; 21.05.2011
comment
ну, это обсуждение не имеет никакого смысла, но для ясности: вызов некоторых функций после определенного тайм-аута не очень хорошо определен в gen_srv. Где в fsm мы ожидаем другого состояния после заданного тайм-аута. - person user425720; 22.05.2011

Честно говоря, я не вижу смысла в разделении инициализации. Выполнение тяжелой работы в init приводит к зависанию супервизора, но использование timeout/handle_info, отправка сообщения в self() или добавление init_check в каждый обработчик (еще одна возможность, хотя и не очень удобная) эффективно зависают вызывающие процессы. Так зачем мне "работающий" супервизор с "не совсем рабочим" gen_server? Чистая реализация, вероятно, должна включать ответ «not_ready» для любого сообщения во время инициализации (почему бы не вызвать полную инициализацию из init + отправить сообщение обратно в self() после завершения, что сбросит статус «not_ready»), но тогда ответ «не готов» должен быть правильно обрабатывается вызывающей стороной, и это добавляет много сложности. Просто приостанавливать ответ — не лучшая идея.

person Victor Moroz    schedule 19.05.2011
comment
Представьте, что руководитель запускает 100 детей, каждый из которых выполняет инициализацию, связанную с сетью, за полсекунды. Задержка в минуту от супервизора — это плохо, но клиенты, вероятно, могут выдержать задержку в полсекунды от gen_server. - person cthulahoops; 20.05.2011
comment
@cthulahoops Я могу себе это представить (не совсем понимаю, какова природа таких соединений, 100 слушателей?), Но я считаю, что полсекунды - это не процессорное время. Так почему бы не запустить все 100 параллельно? Это будут те же полсекунды. - person Victor Moroz; 20.05.2011
comment
Цель состоит в том, чтобы запустить их все параллельно, но супервизор не запустит второй дочерний процесс, пока первый не вернется из инициализации. Следовательно, полезно, чтобы init возвращался быстро, даже если gen_servers будет блокировать запросы на короткое время. - person cthulahoops; 20.05.2011
comment
@cthulahoops Инициализируйте все ваши 100 соединений параллельно в инициализации супервизора и передайте их в качестве параметров дочерним спецификациям. Я что-то пропустил? Вы даже можете использовать дочерний модуль для такой инициализации для правильной инкапсуляции. - person Victor Moroz; 21.05.2011