Если функция init/1 в процессе gen_server отправляет сообщение самому себе, гарантированно ли он прибыть перед любым другим сообщением?

Есть образец, который я видел иногда, когда функция init/1 процесса gen_server отправляет сообщение самому себе, сигнализируя, что оно должно быть инициализировано. Цель этого заключается в том, чтобы процесс gen_server инициализировал себя асинхронно, так что процесс, порождающий его, не должен ждать. Вот пример:

-module(test).
-compile(export_all).

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

handle_cast(init, {}) ->
    io:format("initializing~n"),
    {noreply, lists:sum(lists:seq(1,10000000))};
handle_cast(m, X) when is_integer(X) ->
    io:format("got m. X: ~p~n", [X]),
    {noreply, X}.

b() ->
    receive P -> {} end,
    gen_server:cast(P, m),
    b().

test() ->
    B = spawn(fun test:b/0),
    {ok, A} = gen_server:start_link(test,[],[]),
    B ! A.

Процесс предполагает, что сообщение init будет получено до любого другого сообщения, иначе оно будет аварийно завершено. Возможно ли, чтобы этот процесс получил сообщение m перед сообщением init?


Предположим, что нет процесса отправки сообщений в случайные pids, сгенерированные с помощью list_to_pid, так как любое приложение, выполняющее это, вероятно, вообще не работает, независимо от ответа на этот вопрос.

Ответ 1

Теоретический ответ на вопрос: возможно ли, чтобы процесс a получил сообщение перед сообщением init? ДА. Но практически (когда ни один процесс не выполняет list_to_pid и не отправляет сообщение) этому процессу отвечает НЕТ, если gen_server не зарегистрирован.

Это связано с тем, что возврат gen_server: start_link гарантирует, что выполняется callback init gen_server. Таким образом, инициализация сообщения является первым сообщением в очереди сообщений процесса, прежде чем какой-либо другой процесс получит Pid для отправки сообщения. Таким образом, ваш процесс безопасен и не получает никакого другого сообщения перед init.

Но то же самое не относится к зарегистрированному процессу, так как может быть процесс, который может отправлять сообщение gen_server с использованием зарегистрированного имени еще до того, как он завершит функцию init callback. Давайте рассмотрим эту тестовую функцию.

test() ->
    Times = lists:seq(1,1000),
    spawn(gen_server, start_link,[{local, ?MODULE}, ?MODULE, [], []]),
    [gen_server:cast(?MODULE, No) || No <-Times].

Выходной сигнал выборки

1> async_init:test().
Received:356
Received:357
[ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,
 ok,ok,ok,ok,ok,ok,ok,ok,ok,ok|...]
Received:358
Received:359
2> Received:360
2> Received:361
...
2> Received:384
2> Received:385
2> Initializing
2> Received:386
2> Received:387
2> Received:388
2> Received:389 
...

Вы можете видеть, что gen_server получил сообщения от 356 до 385 сообщений до инициализации. Таким образом, асинхронный обратный вызов не работает в сценарии зарегистрированных имен.

Это можно решить двумя способами:

1.Узнайте процесс после возвращения Pid.

 start_link_reg() ->
      {ok, Pid} = gen_server:start(?MODULE, [], []),
      register(?MODULE, Pid).

2.Or в handle_cast для сообщения init зарегистрирует процесс.

handle_cast(init, State) ->
    register(?MODULE, self()),
    io:format("Initializing~n"),
    {noreply, State};

Результат выборки после этого изменения

1> async_init:test().
Initializing
Received:918
Received:919
[ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,ok,
 ok,ok,ok,ok,ok,ok,ok,ok,ok,ok|...]
Received:920
2> Received:921
2> Received:922
...

Таким образом, отправка сообщения для себя для инициализации не гарантирует, что это первое сообщение, которое он получает, но с бит изменений в коде (и дизайне) может гарантировать, что он будет первым, который будет выполнен.

Ответ 2

В этом конкретном случае вы были бы в безопасности, полагая, что сообщение 'init' будет получено до 'm'. В целом (и особенно, если вы регистрируете свой процесс), это не так.

Если вы хотите быть на 100% безопасным, зная, что ваш код инициализации будет работать первым, вы можете сделать что-то вроде:

start_link(Args...) ->
    gen_server:start_link(test, [self(), Args...], []).

init([Parent, Args...]) ->
    do_your_synchronous_start_stuff_here,
    proc_lib:init_ack(Parent, {ok, self()}),
    do_your_async_initializing_here,
    io:format("initializing~n"),
    {ok, State}.

Я не тестировал это, поэтому не знаю, будет ли "бонусный" init_ack печатать уродливое сообщение на терминал или нет. Если это так, код нужно немного расширить, но общая идея все же стоит. Дайте мне знать, и я уточню свой ответ.

Ответ 3

Код примера безопасен и m всегда принимается после init.

Однако, с точки зрения Теоретическая, если init/1 обработчик gen_server отправляет сообщение самому себе, используя gen_server:cast/2 или примитив отправки, не гарантируется, чтобы быть первым сообщением.

Невозможно гарантировать это просто потому, что init/1 выполняется в процессе gen_server, поэтому после создания процесса и выделения pid и почтового ящика. В режиме, отличном от SMP, планировщик может планировать процесс под некоторой нагрузкой до вызова функции init или до отправки сообщения, поскольку вызов функции (например, gen_server:cast/2 или обработчик инициализации если на то пошло) генерирует редукцию, и эмулятор BEAM проверяет, пришло ли время, чтобы дать некоторое время другим процессам. В режиме SMP у вас может быть другой планировщик, который будет запускать некоторый код, отправляющий сообщение в ваш процесс.

То, что отличает теорию от практики, - это способ узнать о существовании процесса (чтобы отправить ему сообщение перед сообщением init). Код может использовать ссылки от супервизора, зарегистрированного имени, списка процессов, возвращаемых erlang:processes() или даже вызвать list_to_pid/1 со случайными значениями или unserialize pids с binary_to_term/1. Ваш node может даже получить сообщение от другого node с сериализованным pid, особенно учитывая, что число создания обертывается после 3 (см. Ваш другой вопрос Неверный процесс, убиваемый другим node?).

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

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

Если gen_server не зарегистрирован или указан по имени, клиенты в конечном итоге передадут pid на gen_server:call/2,3 или gen_server:cast/2, которые они получат через супервизор, который вызывает gen_server:start_link/3. gen_server:start_link/3 возвращается только после возврата init/1 и, следовательно, после сообщения init. Это именно то, что делает ваш код выше.

Ответ 4

gen_server использует proc_lib: init_ack, чтобы убедиться, что процесс запущен правильно, прежде чем вернуть pid из start_link. Таким образом, сообщение, отправленное в init, будет первым сообщением.

Ответ 5

Это не 100% безопасно! В строке gen.erl 117-129 мы можем видеть это:

init_it(GenMod, Starter, Parent, Mod, Args, Options) ->
init_it2(GenMod, Starter, Parent, self(), Mod, Args, Options).

init_it(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
    case name_register(Name) of
        true ->
            init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options);
        {false, Pid} ->
            proc_lib:init_ack(Starter, {error, {already_started, Pid}})
    end.

init_it2(GenMod, Starter, Parent, Name, Mod, Args, Options) ->
    GenMod:init_it(Starter, Parent, Name, Mod, Args, Options).

В init_it/7 сначала регистрируется его Имя, а затем в init_it2/7 он вызывает GenMod:init_it/6, в котором он вызывает вашу функцию init/1.

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

Решение Daniel может быть правильным, но я не совсем уверен, что две ошибки proc_lib:init_ack вызовут ошибку или нет. Однако родитель никогда не хотел бы получать неожиданное сообщение. > _ & Л;

Вот другое решение. Держите флаг в gen_servser состоянии, чтобы отметить, инициализирован ли сервер. И когда вы получите m, просто проверьте, инициализирован ли сервер, если нет, gen_cast m.

Это немного хлопотное решение, но я уверен, что это правильно. = _ =

Я новичок здесь, как бы я хотел добавить комментарий. > "& Л;