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

Posted

技术标签:

【中文标题】如果 gen_server 进程中的 init/1 函数向自己发送一条消息,它是不是保证在任何其他消息之前到达?【英文标题】:If the init/1 function in a gen_server process sends a message to itself, is it guaranteed to arrive before any other message?如果 gen_server 进程中的 init/1 函数向自己发送一条消息,它是否保证在任何其他消息之前到达? 【发布时间】:2013-08-02 13:37:28 【问题描述】:

我偶尔会看到一种模式,其中gen_server 进程的init/1 函数将向自身发送一条消息,表明它应该被初始化。这样做的目的是让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 消息将在任何其他消息之前被接收 - 否则它将崩溃。这个过程是否有可能在init消息之前得到m消息?


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

【问题讨论】:

另见this question。 我无法判断这些答案是否正确,因为它们似乎都假设这个问题是正确的:***.com/q/18018780/2213023 【参考方案1】:

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

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

但是注册进程的情况并非如此,因为可能有一个进程可能在完成回调初始化函数之前使用注册名称向 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
...

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

【讨论】:

前提,“gen_server:start_link的返回保证了gen_server的回调init被执行”。并不暗示结论,“因此,初始化消息是进程消息队列中的第一条消息,然后任何其他进程获取 Pid 发送消息。”见***.com/q/18018780/2213023。但也许 gen_server 可以通过其他方式做出保证。 即使根据link。 handle_cast 将确保在执行下一次 init 调用(返回)之前将消息发送到邮箱。因此,在 start_link return 之前发送消息的组合确保这是在调用者获取 Pid 发送下一条消息之前消息队列中的第一条消息。 我猜你的意思是“邮件之前放入到邮箱”,而不是“邮件之前发送到邮箱”。有很大的不同。【参考方案2】:

在这种特殊情况下,假设“init”消息将在“m”之前收到是安全的。但总的来说(尤其是如果您注册了您的进程)这不是真的。

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

【讨论】:

确实,这种方法意味着向主管发送一个额外的消息,这是一个未定义的行为。从技术上讲,supervisor 只会将消息输出到标准输出,但它可能会崩溃或解释此消息。或者,您可以从 gen_server 重写逻辑并调用proc_lib:start_link/3,4,5,注册名称(如果需要),然后调用proc_lib:init_ack/1,2,执行异步初始化并最终调用gen_server:enter_loop/3,4,5。实际上,这不是必需的,并且在init/1 中向自己发送消息是安全的。 专注于可以。有很多情况是不安全的。【参考方案3】:

您的示例代码是安全的,并且总是在init 之后收到m

但是,从理论上的角度来看,如果 gen_server 的 init/1 处理程序使用 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。您的节点甚至可能从另一个具有序列化 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,它们将通过调用 gen_server:start_link/3 的主管获得。 gen_server:start_link/3 仅在 init/1 返回时返回,因此在 init 消息入队之后返回。这正是您上面的代码所做的。

【讨论】:

"代码可以设计成在初始化之前不允许消息到达。"我不确定我是否理解这一点。您的意思是“可以在设计代码时假设首先收到init 消息”? 实际上,我的意思是可以将代码设计为确保首先收到初始化消息。我相应地编辑了答案。【参考方案4】:

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

【讨论】:

注册进程怎么样?我可以在init 完成之前向进程名称发送消息吗? 回答我自己的问题,如果您在init函数向自己发送B消息之前向注册进程发送消息A,则A消息将在@987654326之前处理@. 这个答案不正确。在进程正确启动之前,有多种方法可以向进程发送消息。最简单的是 erlang:processes() 或者查看主管的链接。这在注册进程的情况下甚至很常见,因为这发生在 init 运行之前。【参考方案5】:

不是 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 中,进程首先注册它的Name,然后在init_it2/7 中调用GenMod:init_it/6,并在其中调用您的init/1 函数。

虽然,在 gen_server:start_link 返回之前,很难猜到新的进程 id。但是,如果您使用已注册的名称向服务器发送消息,并且消息在调用 gen_server:cast 之前到达,则您的代码将是错误的。

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

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

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

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

【讨论】:

解决重复的 init_ack 很简单,大约 5-10 行代码。这不是让 每次 gen_server 收到消息都需要检查标志的借口。 是的,添加标志不是一个好的解决方案,我同意...但它实际上是另一种解决方案...=_=啊哈! 感谢您的评论~对我这样的新手来说真的是一种鼓励!

以上是关于如果 gen_server 进程中的 init/1 函数向自己发送一条消息,它是不是保证在任何其他消息之前到达?的主要内容,如果未能解决你的问题,请参考以下文章

Erlang gen_server:如何捕获错误?

Erlang,尝试制作 gen_server: 调用有很多响应

发送消息时确保 gen_fsm/gen_server 进程存在

Erlang:无法创建 gen_server:call()

主管不会在 econnrefused 上重新启动(在 init/1 中抛出)

在 init_per_suite 中创建的命名 gen_server 进程在测试中不存在