Erlang:两阶段init安全吗?

时间:2016-09-26 08:36:41

标签: erlang otp

使用gen_server时,有时我需要做一个"两阶段初始化"或者"分离的init",它是这样的:

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

b)之后,self() ! init_stage2被称为

c)init/1返回{ok, PartiallyInitializedState}

d)在将来的某个时刻,调用handle_info/2来处理b)中发送的init_stage2消息,从而完成启动过程。

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

根据10.8 Is the order of message reception guaranteed?,(引用,如下),这是可能的,(如果我理解的话),但我不能产生失败(c之间的请求)和d)处理部分启动状态)

  

是的,但只在一个过程中。

     

如果有实时流程并且您发送消息A然后消息B,则保证如果消息B到达,则消息A到达它之前。

     

另一方面,假设进程P,Q和R.P将消息A发送到Q,然后将消息B发送到R.不能保证A在B之前到达。(如果分布式Erlang将会非常艰难,这是必需的!)

下面是我用来尝试在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,它& #39; s保证如果消息B到达,消息A就会到达它。" 它没有说谁发送了A,谁发送了B,我想这意味着它没有&#39重要的是,只要他们被送到同一个过程,在这种情况下,两阶段的初始练习是安全的。无论如何,如果一些Erlang / OTP大师可以澄清这一点,那就太好了。

(关于主题,说" Erlang / OTP"感觉就像那些GNU人强迫你说'#34; GNU Linux": - )

编辑2 感谢@Dogbert,可以通过以下两种方式说明此问题的简短版本:

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

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

2 个答案:

答案 0 :(得分:1)

在您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的名义提及包括很多他们的工作.Erlang是语言,OTP是实用程序和模块的分发,但它们都是同一群人的工作,所以他们可能会原谅你。)

ad 1)不,这不是保证,它是当前实施的一种方式,并且在预见的未来不太可能改变,因为它简单而强大。当进程将消息发送到同一VM中的进程时,它会将消息术语复制到分离的堆/环境中,然后以原子方式将消息附加到消息框的链接列表中。如果进程将消息发送给自己,我不确定是否复制了消息。有共享堆实现,它不复制消息,但这些细节都没有改变这一事实,该消息在进程继续工作之前链接到接收者的消息框。

ad 2)首先,您知道B如何在A发送消息后发送消息?想一想。然后我们可以谈谈MasgA和MsgB。不能保证MsgA在MsgB之前到达,特别是如果A,B和P各自在不同的VM上,尤其是不同的计算机上。 A发送MsgA后保证B发送消息MsgB的唯一方法是在A向Ms发送MsgA后从A发送MsgC,但即使B在接收到MsgC后将MsgB发送给P,也不能保证P之前收到MsgA MSGB。所以在场景A中发送MsgA到P然后MsgC发送到B和B接收MsgC然后发送MsgB到P你知道在MsgB之前发送了MsgA但是在极少数情况下P仍然可以在MsgA之前收到MsgB,当A,B和P打开时由网络连接的不同计算机。当A,B和P在同一个虚拟机上时,它应该永远不会发生,这是由于如何实现消息发送。

答案 1 :(得分:0)

可以在两个步骤之间接收消息,即使它不太可能,特别是如果服务器没有注册(因为另一个进程很少有机会使用服务器的pid),但是注册过程它例如,当主管在应用程序的某个其他部分重新启动服务器时。为了展示它的实际应用,我写了一个小例子,系统地失败了(当作为测试(坏)启动时)。

为了实现它,我在init函数中插入了一个100ms的睡眠。它为另一个进程提供了在初始化时向服务器发送消息的时间,并且可以模拟一个长初始化函数。

我使用第二个进程先生成并每隔10ms向服务器发送一条消息。该消息使用已注册的名称,因此我不需要知道pid,它是在catch块中发送的,因为由于服务器尚未注册,所以第一个消息发送将会失败。

-module(twosteps).

-behaviour(gen_server).

%% export interfaces
-export([start_link/0,test/1,stop/0,badguy/0]).

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

%% INTERFACES %%

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

stop() ->
    gen_server:stop(?MODULE).

test(good) ->
    start_link(),
    ?MODULE ! this_will_work,
    stop();
test(bad) ->
    Pid = spawn(?MODULE,badguy,[]),
    start_link(),
    stop(),
    Pid ! stop.


%% CALLBACKS %%

init(_) ->
    timer:sleep(100),
    self() ! finish_init,
    {ok, first_step}.

handle_call(_Request, _From, State) ->
    {reply, {error, unknown_call}, State}.

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

handle_info(finish_init, first_step) ->
    io:format("Init OK~n"),
    {noreply, init_done};
handle_info(finish_init, too_late) ->
    io:format("iznogoud~n"),
    {noreply, too_late};
handle_info(_Info, first_step) ->
    io:format("Init KO, state is first_step~n"),
    {noreply, too_late};
handle_info(_Info, State) ->
    io:format("State is ~p~n",[State]),
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

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

%% LOCAL FUNCTIONS %%

badguy() ->
    catch (?MODULE ! this_will_fail),
    receive
        stop -> ok
    after 10 ->
        badguy()
    end.

行动中:

1> c(twosteps).
{ok,twosteps}
2> twosteps:test(good).
Init OK
State is init_done
ok
3> twosteps:test(bad). 
Init KO, state is first_step
State is too_late
State is too_late
State is too_late
State is too_late
State is too_late
iznogoud
State is too_late
State is too_late
stop
4>