Неправильно ли отправлять сообщение self() в init?
В этом примере автор избегает ситуации взаимоблокировки:
self() ! {start_worker_supervisor, Sup, MFA}
в его функции init gen_server. Я сделал что-то подобное в одном из моих проектов, и мне сказали, что этот метод не одобряется, и что вместо этого было бы лучше немедленно использовать тайм-аут. Что такое принятый шаблон?
Ответы
Ответ 1
Обновление для Erlang 19 +
Рассмотрим использование нового gen_statem
. Это поведение поддерживает генерацию событий, внутренних для FSM:
Функция состояния может вставлять события, используя action() next_event, и такое событие вставляется в качестве следующего в функцию состояния. То есть, как будто это старейшее входящее событие. Для таких событий можно использовать выделенный event_type() internal, что делает невозможным ошибку для внешних событий.
Вставка события заменяет трюк вызова собственных функций обработки состояния, к которым вам часто приходилось прибегать, например 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, не должен ждать событий, это то, за что обращаются вызовы handle.
Итак, в этом конкретном случае генерируется своеобразное "поддельное" событие. Я бы сказал, что кажется, что gen_server
всегда хочет инициировать запуск супервизора. Почему бы просто не сделать это прямо в init/1
? Есть ли на самом деле требование иметь возможность обрабатывать другое сообщение между ними (эффект от этого в handle_info/2
)? Этот ветер настолько невероятно мал (время между началом gen_server
и отправкой сообщения на self()
), так что это вряд ли произойдет вообще.
Что касается тупика, я бы посоветовал не называть своего супервизора в вашей функции init. Это просто плохая практика. Хорошим шаблоном проектирования для начинающего рабочего процесса будет один руководитель высшего уровня, с менеджером и руководителем работника ниже. Менеджер запускает рабочих, вызывая диспетчера работника:
[top_sup]
| \
| \
| \
man [work_sup]
/ | \
/ | \
/ | \
w1 ... wN
Ответ 2
Просто чтобы дополнить уже сказанное о разделении инициализации серверов на две части, первая в функции init/1
, а вторая - в handle_cast/2
или handle_info/2
. Существует только одна причина для этого, и это значит, что инициализация, как ожидается, займет много времени. Затем его расщепление позволит gen_server:start_link
вернуться быстрее, что может быть важно для серверов, запущенных супервизорами, поскольку они "зависают" при запуске своих детей, а один медленный стартовый ребенок может задержать запуск всего супервизора.
В этом случае я не думаю, что это плохой стиль, чтобы разделить инициализацию сервера.
Важно быть осторожным с ошибками. Ошибка в init/1
приведет к завершению работы супервизора во время ошибки во второй части, поскольку они заставят супервизора попробовать и перезагрузить этот дочерний элемент.
Я лично считаю, что лучше настроить сервер для отправки сообщения самому себе, либо с явным !
, либо gen_server:cast
, как с хорошим описательным сообщением, например init_phase_2
, это будет проще чтобы увидеть, что происходит, а не более анонимный тайм-аут. Особенно, если тайм-ауты используются и в других местах.
Ответ 3
Вызов собственного супервайзера наверняка кажется плохой идеей, но я все время делаю что-то подобное.
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 до тех пор, пока мы не отправим это сообщение), и он не встаньте, если мне нужно использовать таймауты для чего-то еще.
Ответ 4
Это может быть очень эффективное и простое решение, но я думаю, что это не хороший стиль erlang.
Я использую таймер: apply_after, что лучше и не создает впечатление взаимодействия с внешним модулем /gen _ *.
Я думаю, что лучший способ - использовать государственные машины (gen_fsm). Большинство наших gen_srvers - это действительно машина состояний, однако из-за первоначальной работы по настройке get_fsm я думаю, что мы закончили с gen_srv.
В заключение я бы использовал таймер: apply_after, чтобы сделать код понятным и эффективным, или gen_fsm, чтобы быть чистым стилем Erlang (даже быстрее).
Я только что прочитал фрагменты кода, но сам пример как-то сломался - я не понимаю эту конструкцию gen_srv, манипулирующей супервизором. Даже если он является менеджером некоторого пула будущих детей, это еще более важная причина, чтобы сделать это явно, не считая магов почтовых ящиков процессов. Отладка этого будет также адом в какой-то более крупной системе.
Ответ 5
Честно говоря, я не вижу смысла в расщеплении инициализации. Выполнение тяжелой работы в init
выполняется в режиме наблюдения, но используя timeout/handle_info
, отправляя сообщение на self()
или добавляя init_check
к каждому обработчику (другая возможность, но не очень удобная), эффективно зависает процессов вызова. Так почему мне нужен "рабочий" супервизор с "не совсем работающим" gen_server? Чистая реализация должна, вероятно, включать "not_ready" ответ для любого сообщения во время инициализации (почему бы не запустить полную инициализацию из init + отправить сообщение обратно на self()
по завершении, что будет reset "not_ready" ), но затем "не готов" "Ответ должен быть правильно обработан вызывающим абонентом, и это добавляет много сложности. Просто приостановка ответа - это не очень хорошая идея.