【问题标题】:Erlang: delayed initialization in gen_serverErlang:在 gen_server 中延迟初始化
【发布时间】:2016-03-24 19:26:22
【问题描述】:

假设我有一个 gen_server 回调模块,g,sn-p 的代码如下所示:

start_link(Args) ->
    gen_server:start_link(?MODULE, [Args], []).

process_packet(Ref, Packet) ->
    gen_server:call(Ref, MsgPacket={process_packet, Packet}).

init(Args) ->
    gen_server:cast(self(), MsgInit={init, Args}), %% delayed initialization
    {ok, state_not_initialized}.

handle_call({process_packet, Packet}, #g_state{}=S) ->
    {reply, Packet, S}.

handle_cast({init, Args}, _) ->
    State = #g_state{} = do_init(Args),
    {noreply, State}.

还有另一个 gen_server,t,它的工作是监听一个套接字, 如果收到一个特定的数据包,启动一个g 来处理这个数据包, 所以,t 中的一些代码看起来像这样:

handle_info({tcp, _Socket, Packet}, #t_state{}) ->
    case g:start_link(WhatEver) of
        {ok, Pid} ->
            g:process_packet(Pid, Packet);
        _ ->
            not_interested
    end.

g 的 pid 为 PidGt 的 pid 为 PidT

我的问题是,MsgPacket(由PidT 发送给PidG)是否有可能在MsgInit(由PidG 发送给它自己)之前到达PidG?如果发生这种情况,PidG 将崩溃,因为state_not_initializedghandle_call 中的#g_state{} 不匹配。

我的猜测是这完全有可能,但我没有想出一种方法来产生这种情况。理想情况下,您可以减慢消息MsgInit 的消息传输速度,但我怀疑 Erlang 是否允许我做这种事情。知道如何让MsgPacketMsgInit 之前到达吗?

修复相对容易,(假设我的猜测是正确的),您只需在g 启动之后,PidGdo_initPidT 中发送receive 一些ack,之前进行 gen 调用。

更新

假设我的猜测是正确的,为了使问题更具体,如何使kickoff_many/1 启动的进程之一崩溃? (根据zxq9的例子修改)

-module(spawn_spammer).
-export([kickoff_many/1]).

kickoff() ->
    {ok, Catcher} = spawn_catcher:start(),
    {echo, _} = spawn_catcher:process_packet(Catcher, {packet_from, self()}).

kickoff_many(N) ->
    lists:foreach(fun(_) -> spawn(fun kickoff/0) end, lists:seq(1, N)).


-module(spawn_catcher).
-behavior(gen_server).

-export([start/0,
         process_packet/2,
         init/1,
         handle_call/3, handle_cast/2, handle_info/2,
         terminate/2, code_change/3]).

start() ->
    gen_server:start(?MODULE, [], []).

process_packet(Ref, Packet) ->
    gen_server:call(Ref, {process_packet, Packet}).

init(_) ->
    gen_server:cast(self(), get_ready),
    {ok, not_ready}.

handle_cast(get_ready, not_ready) ->
    {noreply, ready}.

handle_call({process_packet, P}, _From, ready) ->
    {stop, normal, {echo, P}, ready};

handle_call({process_packet, _P}, _From, not_ready) ->
    {stop, normal, call_while_not_ready, not_ready}.

handle_info(_, ready) ->
    {stop, normal, unexpected, ready}.

terminate(_, _) -> ok.

code_change(_, State, _) -> {ok, State}.

【问题讨论】:

    标签: erlang erlang-otp


    【解决方案1】:

    是的,这是可能的。你可以知道消息序列相对于进程A和B,但是你不能知道消息序列相对于任何其他两个进程A和B(意思是,你可以'不知道各种消息流的交错顺序),这也意味着你不知道消息从 A 到 B 以及 B 到 B 的相对顺序。

    如何避免这种情况?最直接的方法是让你的进程 T 如果我还不存在,则派生进程 G。或者总是产生一个 G 来处理处理工作。或者自己处理处理。或者使套接字侦听器不是一个 gen_server,而是一个纯 Erlang OTP 进程(查找 proc_lib——我发现这对于套接字侦听器来说更顺畅)以及您在 gen_server 世界和可以消除初始化套接字处理程序进程某些方面的必要性。

    总结一下:

    • 您认为存在的排序问题确实存在。在实践中,它几乎不会发生——当然,直到生产中最不合时宜的时间。
    • 套接字处理程序除了处理套接字之外什么都不做。
    • “处理套接字”是指在 Erlang 消息和网络消息之间进行转换(如果它的 TCP 它们不是数据包它是一个流 -- 小心),并分派到内部 gen_* 进程。李>
    • 可以处理您的初始化问题,但最好完全消除需要。几乎总有办法做到这一点——如果没有,就有proc_lib

    更新

    如果您不通过选择接收搜索您的神奇第一条消息刷新邮箱会发生以下情况。

    这里我们有一个垃圾邮件进程,它将向即将生成的 gen_server 发送 bajillion 消息:

    -module(spawn_spammer).
    -export([kickoff/0]).
    
    kickoff() ->
        Spammer = start(),
        Catcher = spawn_catcher:start(),
        {Spammer, Catcher}.
    
    start() ->
        ok = io:format("~tp ~tp: Starting up.~n", [self(), ?MODULE]),
        spawn(fun() -> loop() end).
    
    loop() ->
        try
            spawn_catcher ! {self(), test_spam}
        catch
            _:_ -> io:format("~tp ~tp: Missed.~n", [self(), ?MODULE])
        end,
        receive
            cut_it_out ->
                ok;
            Unexpected ->
                io:format("~tp ~tp: Unexpected message ~tp~n", [self(), ?MODULE, Unexpected]),
                loop()
            after 0 ->
                loop()
        end.
    

    这里我们有那个 gen_server,试图用它自己进行一个(假定的)安全延迟初始化:

    -module(spawn_catcher).
    -behavior(gen_server).
    -export([start/0, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
    
    start() ->
        ok = io:format("~tp ~tp: Starting up.~n", [self(), ?MODULE]),
        gen_server:start({local, ?MODULE}, ?MODULE, [], []).
    
    init(_) ->
        ok = io:format("~tp ~tp: Blank initialization.~n", [self(), ?MODULE]),
        gen_server:cast(self(), get_ready),
        {ok, not_ready}.
    
    handle_call(Message, From, State) ->
        ok = io:format("~tp ~tp: Unexpected call: ~tp from ~tp~n", [self(), ?MODULE, Message, From]),
        {noreply, State}.
    
    handle_cast(get_ready, not_ready) ->
        ok = io:format("~tp ~tp: Getting ready~n", [self(), ?MODULE]),
        {noreply, ready};
    handle_cast(Message, State) ->
        ok = io:format("~tp ~tp: Unexpected call: ~tp~n", [self(), ?MODULE, Message]),
        {noreply, State}.
    
    handle_info(Message, not_ready) ->
        ok = io:format("~tp ~tp: DANGEROUS MESSAGE: ~tp~n", [self(), ?MODULE, Message]),
        {noreply, not_ready};
    handle_info({Spammer, test_spam}, ready) ->
        ok = io:format("~tp ~tp: Got first proper message. Sending reply.~n", [self(), ?MODULE]),
        Spammer ! cut_it_out,
        {stop, normal, ready};
    handle_info(Message, ready) ->
        ok = io:format("~tp ~tp: Unexpected message: ~tp~n", [self(), ?MODULE, Message]),
        {noreply, ready}.
    
    terminate(_, _) -> ok.
    
    code_change(_, State, _) -> {ok, State}.
    

    这就是它在 shell 中的表现:

    1> spawn_spammer:kickoff().
    <0.33.0> spawn_spammer: Starting up.
    <0.33.0> spawn_catcher: Starting up.
    <0.99.0> spawn_spammer: Missed.
    <0.99.0> spawn_spammer: Missed.
    <0.100.0> spawn_catcher: Blank initialization.
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    {<0.99.0>,{ok,<0.100.0>}}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: DANGEROUS MESSAGE: {<0.99.0>,test_spam}
    <0.100.0> spawn_catcher: Getting ready
    <0.100.0> spawn_catcher: Got first proper message. Sending reply.
    

    这极不可能,但它可能发生。

    【讨论】:

    • 我认为这是不正确的。详情请查看我的回答。
    • 编辑后的示例包含对 erlang:register/2 的调用。这不是作者提供的示例。
    • @PaulPeregud 提供的示例调用了 gen_server:spawn/4,在使用延迟初始化时几乎没有什么奇怪的事情。事实仍然是,仅使用 gen_server 定义的启动调用是可能的。它是完全适用的。此外,我不止一次地在生产中看到过这个确切的错误。此外,Pid 被回收,因此您可以获得对很久以前向太空发送消息的 Pid 的杂散引用,然后突然发送到某个存在的地方。我在生产中也看到了。 99.999% 的时间这是可以的。但它可能发生。
    • @zxq9 您在示例中使用了一个技巧(注册进程名称),让您尽快将消息发送到 gen 服务器,这会导致排序问题:如果您使用 pid 而不是 name,您可以仅在评估 gen_server:cast(self(), MsgInit={init, Args}), %% A 之后才与 PidG 通信(尽管不必传递 MsgInit,也许它会在传递 MsgPacket 之后传递),而在您的示例中,将在 A 之前评估 spawn_catcher ! blah执行。
    • ...我知道这种推理是有缺陷的,10.8 Is the order of message reception guaranteed 没有涵盖 P 到 P 和 Q 到 P 的情况,最好保留评估的顺序。
    【解决方案2】:

    简短的回答。不,这是不可能的。

    这种技术有时被称为延迟初始化,并且经常使用。

    长答案。不,如果您使用的是 gen_server 而不是选择性接收,则这是不可能的。

    来自 gen_server 文档:

    gen_server 进程调用 Module:init/1 进行初始化。为确保同步启动过程,start_link/3,4 在 Module:init/1 返回之前不会返回。

    您应该在这里考虑的事情如下。

    g:init/1 运行时谁知道 PidG?只有 PidG 进程本身。只有在 PidG 从 g:init/1 返回之后,启动 PidG 的进程才会从 gen_server:start/x 返回。

    确定后,我们知道 {init, Args} 是 PidG 消息队列中的第一条消息。由于我们使用的是 gen_server,它(通常)一个接一个地处理消息,您可以确定 PidG 也会将消息 {init, Args} 作为第一个处理。

    此时 PidG 和 PidT 之间没有交互。

    编辑:删除有关返回 {ok, State, 0} 和接收“超时”的提及,因为它会因为创建竞争条件而失败(请参阅 gen_server.erl)。只有一种安全的方法:self()!在您不执行诸如在 Module:init/x 中的任何位置发送您的 pid 之类的事情的情况下发送消息。

    【讨论】:

    • PID 存在的时刻是一个有效的目标。在 self() 发送的消息之前,消息不太可能到达那里,但有可能发生 - 不能保证发送之间没有延迟并接收。我不相信这与 init() 返回时除了 PID 不会返回到任何称为 start 函数的情况有任何关系。但是,PID 在返回之前存在(否则对 self() 的调用将不起作用),并且如果进程已注册,例如,该名称可以在 init() 完成之前解析。
    • 这些场景都不适用于@"Not an ID" 提供的例子 问题是关于延迟初始化,一般是安全的。
    • IMO 这是一个丑陋的黑客攻击,由于在 OTP 中定义通用服务的方式,在极少数情况下强制程序员使用,并且有一个更强大的解决方法以 proc_lib 的形式。它被搞砸的可能性是微乎其微的,通常低于Pid = spawn(Foo), link(Pid) 而不是spawn_link 可能失败的情况(甚至曾一度被误认为是安全情况),但是说“这不可能发生”是错误的。检查我的答案更新。
    • 我认为这与同步初始化无关。
    猜你喜欢
    • 2020-05-10
    • 1970-01-01
    • 2011-11-17
    • 1970-01-01
    • 2020-10-08
    • 2017-11-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多