14

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


让我们假设没有进程向 生成的随机 pid 发送消息list_to_pid,因为无论这个问题的答案如何,任何这样做的应用程序都可能根本不起作用。

4

5 回答 5

5

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

这是因为 gen_server:start_link 的返回保证了 gen_server 的回调 init 被执行。因此,初始化消息是进程消息队列中的第一条消息,在任何其他进程获取 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
...

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

于 2013-09-21T03:44:18.460 回答
2

在这种特殊情况下,假设“init”消息将在“m”之前收到,您将是安全的。一般来说(特别是如果你注册你的进程)这是不正确的。

如果您想 100% 安全地知道您的 init 代码将首先运行,您可以执行以下操作:

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

于 2013-08-01T17:50:18.893 回答
1

您的示例代码是安全的,并且m总是在init.

但是,从理论上讲,如果init/1gen_server 的处理程序使用gen_server:cast/2或 send 原语向自己发送消息,则不能保证是第一条消息。

没有办法简单地保证这一点,因为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 的节点收到消息,特别是考虑到创建编号在 3 之后环绕(请参阅您的另一个问题Wrong process getting blocked on other node?)。

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

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

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

于 2013-09-18T15:32:10.307 回答
0

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

于 2013-08-01T16:25:10.907 回答
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。但是,如果您使用已注册的 Name向服务器发送消息,并且消息在您的 gen_server:cast 被调用之前到达,那么您的代码将是错误的。

丹尼尔的解决方案可能是对的,但我不太确定两个是否proc_lib:init_ack会导致错误。但是,父母永远不希望收到意外的消息。>_<

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

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

我是这里的新生,我多么希望我能添加评论。>"<

于 2013-08-01T18:01:36.777 回答