使用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
,但是它一直隐藏在那里,直到返回为止。因此,您很安全,因为没有人可以call
,cast
或向您发送不知道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/