使用gen_server时,有时我需要执行“两阶段初始化”或“分离初始化”,如下所示:

a)在gen_server回调模块的init/1中,仅完成了部分初始化

b)之后,调用self() ! init_stage2
c)init/1返回{ok, PartiallyInitializedState}
d)在将来的某个时刻,调用handle_info/2来处理b)中发送的init_stage2消息,从而完成初始化过程。

我的主要关注点是,如果在c)和d)之间创建了gen服务器call/cast/info,是否可以使用PartiallyInitializedState处理该请求?

根据10.8 Is the order of message reception guaranteed?(在下面引用),这是有可能的(如果我理解正确的话),但是我无法产生故障(c之间的请求)并且d)使用部分初始化状态进行处理)



以下是我用来尝试在c)和d)之间进行处理的一些代码,但是当然失败了,否则,在这里我不会问这个问题。 (如果您有兴趣,请使用test:start(20000)运行)

%% file need_two_stage_init.erl
-module(need_two_stage_init).

-behaviour(gen_server).

-export([start_link/0]).

-export([init/1, terminate/2, code_change/3,
         handle_call/3, handle_cast/2, handle_info/2]).


start_link() ->
    gen_server:start_link(?MODULE, {}, []).


init({}) ->
    self() ! go_to_stage2,
    %% init into stage1
    {ok, stage1}.

handle_call(_Request, _From, Stage) ->
    {reply, Stage, Stage}.

%% upon receiving this directive, go to stage2,
%% in which the gen_server is fully functional
handle_info(go_to_stage2, stage1) ->
    {noreply, stage2}.

handle_cast(Request, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ignore.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.



%% file test.erl
-module(test).

-export([start/1]).

start(Amount) ->
    start_collector(Amount), %% report the result
    start_many_gens(Amount).

start_collector(Amount) ->
    spawn(fun() ->
                  register(collector, self()),
                  io:format("collector started, entering receive loop~n"),
                  loop(Amount)
          end).

loop(0) ->
    io:format("all terminated~n"),
    all_terminated;
loop(N) ->
    %% progress report
    case N rem 5000 == 0 of
        true -> io:format("remaining ~w~n", [N]);
        false -> ignore
    end,
    receive
        {not_ok, _} = Msg ->
            io:format("======= bad things happened: ~p~n", [Msg]),
            loop(N-1);
        {ok, _} ->
            loop(N-1)
    end.


start_one_gens() ->
    {ok, Pid} = need_two_stage_init:start_link(),
    case gen_server:call(Pid, any) of
        stage2 -> ignore;
        stage1 -> collector ! {not_ok, Pid}
    end,
    gen_server:stop(Pid),
    collector ! {ok, Pid}.


start_many_gens(Amount) ->
    lists:foreach(fun(_) ->
                          spawn(fun start_one_gens/0)
                  end, lists:seq(1, Amount)).

编辑再次阅读以上引用的文档,我认为我确实误会了它,“如果存在实时进程,并且先向消息A发送消息,然后向消息B发送消息,则可以确保消息B到达之前,消息A到达之前。”它没有说谁发送了A,谁发送了B,我想这并不重要,只要他们被发送到同一进程,在这种情况下,两阶段的初始化实践是安全的。无论如何,如果有一些Erlang/OTP专家可以澄清这一点,那就太好了。

(在主题之外,说“Erlang/OTP”的感觉就像是那些GNU家伙强制您说“GNU Linux” :-)

编辑2 感谢@Dogbert,可以使用以下两种方法来说明此问题的简短版本:

1)如果进程向自身发送消息,是否可以保证此消息同步到达邮箱?

2)或者,让A,B和P为三个不同的过程,A先将MsgA发送给P,然后B将MsgB发送给P,是否保证MsgA在MsgB之前到达?

最佳答案

在您的情况下,gen_server:start_link/3不会返回,直到您的need_two_stage_init:init/1返回。因此,要么need_two_stage_init:start_link/0。这意味着您的邮箱中已经有go_to_stage2。因此,当您不使用注册名称时,除了您调用Pid的进程外,没有其他人知道您的gen_server:start_link/3,但是它一直隐藏在那里,直到返回为止。因此,您很安全,因为没有人可以callcast或向您发送不知道Pid的消息。

顺便说一句,返回{ok, PartiallyInitializedState, 0},然后在timeout中处理hanle_info/2,可以达到类似的效果。

(主题之外,Linux是Linus的工作以及周围的小社区,Linux上存在GNU的历史,并且GNU已经建立了具有大量用户空间应用程序的庞大项目,因此它们有充分的理由被OS名称所提及,而OS包含很多(Erlang是语言,而OTP是实用程序和模块的分发,但是它们都是同一组人的工作,因此他们很可能会原谅您。)

广告1)不,它不能保证,这是当前实现的一种方式,并且由于其简单而强大的功能,在可预见的将来也不大可能改变。当进程向同一VM中的进程发送消息时,它将消息项复制到单独的堆/环境中,然后原子地将消息追加到消息框的链接列表中。我不确定过程是否将消息发送给自身时是否复制了消息。有一个共享堆实现,它不复制消息,但是这些细节都不能改变事实,即在过程继续其工作之前,该消息已链接到接收者的消息框。

广告2)首先,您如何知道B在A发送消息后发送消息?想一想。然后我们可以谈谈MasgA和MsgB。不能保证MsgA在MsgB之前到达,特别是如果A,B和P分别在不同的VM(尤其是不同的计算机)上时。保证B在A发送MsgA之后发送消息MsgB的唯一方法是在A向M发送MsgA之后向A发送MsgC,但是即使B在收到MsgC之后向P发送MsgB,也不能保证P在接收MsgA之前就收到MsgA MsgB。因此,在方案A中,先将MsgA发送给P,然后再将MsgC发送给B,然后B接收MsgC,然后再将MsgB发送给P,您知道MsgA是在MsgB之前发送的,但是在极少数情况下,当A,B和P处于打开状态时,P仍然可以在MsgA之前接收MsgB通过网络连接的不同计算机。当A,B和P位于同一VM上时,永远不会发生这种情况,这是由于实现消息发送的方式所致。

关于Erlang:两阶段初始化安全吗?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/39698145/

10-10 08:30