如果gen_server进程中的init / 1函数向自己发送消息,是否保证在任何其他消息之前到达?

时间:2013-08-01 14:47:46

标签: concurrency erlang

我偶尔会看到一种模式,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消息?


假设没有进程向list_to_pid生成的随机pid发送消息,因为执行此操作的任何应用程序可能根本不起作用,无论此问题的答案如何。

5 个答案:

答案 0 :(得分:5)

问题的理论答案是一个进程可以在init消息之前获取消息吗?是。 但实际上(当没有进程正在执行list_to_pid并发送消息时)此过程的答案是,前提是gen_server不是注册过程。

这是因为gen_server:start_link的返回确保执行gen_server的回调初始化。因此,在任何其他进程获得Pid发送消息之前,初始化消息是进程消息队列中的第一条消息。 因此,您的流程是安全的,并且在初始化之前不会收到任何其他消息。

但是注册过程也不会这样,因为可能有一个进程可能会在完成回调init函数之前使用注册名称向gen_server发送消息。 让我们考虑一下这个测试函数。

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.在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
...

因此,向自己发送一条消息进行初始化并不能确保它是它收到的第一条消息,但代码(和设计)中的一些变化可以确保它是第一个被执行的消息。

答案 1 :(得分:2)

在这种特殊情况下,假设在'm'之前收到'init'消息,你会安全的。一般情况下(特别是如果你注册你的过程),这不是真的。

如果您希望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是否会向终端打印一条丑陋的消息。如果是这样,代码必须稍微扩展,但总体思路仍然存在。让我知道,我会更新我的答案。

答案 2 :(得分:1)

您的示例代码是安全的,m后始终会收到init

但是,从理论的角度来看,如果gen_server的init/1处理程序向自己发送消息,使用gen_server:cast/2或发送原语,它不保证是第一条消息。

没有办法保证这一点,因为init/1在gen_server的进程中执行,因此在创建进程并分配了pid和邮箱之后。在非SMP模式下,调度程序可以在调用init函数之前或发送消息之前在某些负载下调度进程,因为调用函数(例如gen_server:cast/2或这个问题的init处理程序)生成一个减少,BEAM仿真器测试是否是时候给其他进程一些时间。在SMP模式下,您可以使用另一个调度程序来运行某些代码,向您的进程发送消息。

理论与实践的区别在于找出流程存在的方式(为了在init消息之前向其发送消息)。代码可以使用来自主管的链接,注册名称,erlang:processes()返回的进程列表,甚至可以使用随机值调用list_to_pid/1或使用binary_to_term/1反序列化pid。您的节点甚至可能从另一个带有序列化pid的节点收到消息,特别是考虑到创建号码在3之后回绕(请参阅另一个问题Wrong process getting killed on other node?)。

这在实践中不太可能。因此,从实际的角度来看,每次使​​用此模式时,可以设计代码以确保首先接收init消息,并在接收其他服务器之前初始化服务器消息。

如果gen_server是注册过程,您可以从主管启动它,并确保之后在监督树中启动所有客户端或引入某种(可能是较差的)同步机制。即使您不使用此异步初始化模式(否则客户端无法访问服务器),这是必需的。当然,在崩溃和重新启动此gen_server的情况下,您可能仍会遇到问题,但无论情况如何,这都是正确的,您只能通过精心设计的监督树进行保存。

如果gen_server未注册或通过名称引用,客户端最终会将pid传递给gen_server:call/2,3gen_server:cast/2,他们将通过调用{ {1}}。 gen_server:start_link/3仅在gen_server:start_link/3返回时返回,因此在init/1消息入队后返回。这正是您上面的代码所做的。

答案 3 :(得分:0)

gen_server使用proc_lib:init_ack确保在从start_link返回pid之前正确启动了该进程。因此,在init中发送的消息将是第一条消息。

答案 4 :(得分:0)

不是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返回之前,很难猜出新的进程ID。但是,如果您使用已注册的名称向服务器发送消息,并且在调用gen_server:cast之前消息到达,则您的代码将出错。

丹尼尔的解决方案可能是正确的,但我不确定两个proc_lib:init_ack是否会导致错误。但是,父母永远不会收到意外的消息。 &GT; _&LT;

这是另一种解决方案。在 gen_servser状态中保留标记,以标记服务器是否已初始化。当您收到m时,只需检查服务器是否已初始化,如果没有,请检查gen_cast m给自己。

这是一个有点麻烦的解决方案,但我确信它是对的。 = _ =

我是新生,我​​希望能添加评论。 &gt;“中&LT;