【问题标题】:Is it bad to send a message to self() in init?在 init 中向 self() 发送消息是不是很糟糕?
【发布时间】:2011-08-28 12:27:53
【问题描述】:

this 示例中,作者通过以下方式避免了死锁情况:

self() ! {start_worker_supervisor, Sup, MFA}

在他的 gen_server 的 init 函数中。我在我的一个项目中做了类似的事情,并被告知这种方法不受欢迎,最好立即超时。接受的模式是什么?

【问题讨论】:

    标签: erlang erlang-otp


    【解决方案1】:

    这可能是非常有效和简单的解决方案,但我认为它不是好的 erlang 风格。 我正在使用 timer:apply_after,它更好,并且不会给人留下与外部模块/gen_* 交互的印象。

    我认为最好的方法是使用状态机 (gen_fsm)。我们的大多数 gen_srvers 都是真正的状态机,但是因为设置 get_fsm 的初始工作,我认为我们最终得到了 gen_srv。

    总而言之,我会使用 timer:apply_after 使代码清晰高效,或者使用 gen_fsm 成为纯粹的 Erlang 风格(甚至更快)。

    我刚刚阅读了代码 sn-ps,但示例本身不知何故被破坏了——我不理解 gen_srv 操纵主管的这种构造。即使它是一些未来孩子池的经理,这也是明确执行此操作的更重要原因,而不依赖于进程的邮箱魔术。在一些更大的系统中调试这也是地狱。

    【讨论】:

    • timer:apply_after 只是一种解决方法,但真正的问题是,在 init 中发送到 self() 是否显示出设计中的根本缺陷。 gen_fsm 也不能解决这个问题,通常不推荐超过 gen_server。
    • 为什么fsm不能解决?重点是执行某些事情并且仍然符合 init/1 规范,该规范预计会返回状态。所以,我们可以返回状态和执行之前的重初始化的实际初始状态。
    • @user4:因为状态函数不会在调用或强制转换或超时时立即调用。就像 gen_server 一样。您可以使用从 init 返回的短暂(例如 0)超时来触发初始化,但您也可以使用 gen_server 来执行此操作。
    • 没错,但在 fsm 中它是明确定义的行为的一部分
    • 在 gen_server 中究竟有什么没有明确定义。
    【解决方案2】:

    Erlang 19+ 更新

    考虑使用新的gen_statem 行为。此行为支持生成 FSM 内部的事件:

    状态函数可以使用 action() next_event 插入事件,并且这样的事件被插入作为下一个呈现给状态函数。也就是说,就好像它是最早的传入事件一样。专用的 event_type() 内部可用于此类事件,使它们不会被误认为是外部事件。

    插入事件取代了调用自己的状态处理函数的技巧,例如,gen_fsm 以强制在其他事件之前处理插入的事件。

    使用该模块中的action functionality,您可以确保您的事件在init 中生成并始终在任何外部事件之前处理,特别是通过在您的init 函数中创建next_event 操作。

    例子:

    ...
    
    callback_mode() -> state_functions.
    
    init(_Args) ->
        {ok, my_state, #data{}, [{next_event, internal, do_the_thing}]}
    
    my_state(internal, do_the_thing, Data) ->
        the_thing(),
        {keep_state, Data);
    my_state({call, From}, Call, Data) ->
        ...
    
    ...
    

    旧答案

    在设计gen_server 时,您通常可以选择在三种不同的状态下执行操作:

    • 启动时,在init/1
    • 运行时,在任何handle_* 函数中
    • 停止时,在terminate/2

    一个好的经验法则是在对事件(调用、转换、消息等)采取行动时在处理函数中执行一些事情。在 init 中执行的东西不应该等待事件,这就是句柄回调的用途。

    因此,在这种特殊情况下,会生成一种“假”事件。我想说gen_server 似乎总是想要启动主管的启动。为什么不直接在init/1 中做呢?是否真的需要能够在两者之间处理另一条消息(而不是在handle_info/2 中执行它的效果)?这个窗口非常小(从gen_server 开始到将消息发送到self() 之间的时间),所以它根本不可能发生。

    至于死锁,我真的建议不要在你的 init 函数中调用你自己的主管。那只是不好的做法。启动工作进程的一个好的设计模式是一个顶级主管,下面有一个经理和一个工作主管。经理通过调用工人主管来启动工人:

    [top_sup]
      |    \
      |     \
      |      \
     man  [work_sup]
           /  |  \
          /   |   \
         /    |    \
        w1   ...   wN
    

    【讨论】:

    • 我不建议将内容移至 init/1 ,因为它通常是重复的代码。当然,我们可以将其提取到内部函数中,但是 handle_call 中的代码可读性较差。
    • 好吧,如果它是重复的代码,你可以把它放在一个函数中,然后从init/1 和你的handle_call/3handle_cast/2 调用它。我认为函数调用比向self() 发送消息更具可读性。
    【解决方案3】:

    打电话给你自己的主管确实看起来是个坏主意,但我一直都在做类似的事情。

    init(...) ->
       gen_server:cast(self(), startup),
       {ok, ...}.
    
    handle_cast(startup, State) ->
       slow_initialisation_task_reading_from_disk_fetching_data_from_network_etc(),
       {noreply, State}.
    

    我认为这比使用 timeout 和 handle_info 更清楚,它几乎可以保证没有消息可以领先于启动消息(在我们发送该消息之前没有其他人拥有我们的 pid),并且它没有如果我需要为其他事情使用超时,请碍事。

    【讨论】:

    • 如果您的gen_servergen_server:start_link/4gen_server:start/4 开头,则注册发生在调用Module:init/1 之前。任何在其注册名称下寻找gen_server 的人都可以找到它并向其发送比您自己的信息更早到达的消息。
    【解决方案4】:

    只是为了补充已经说过的关于将服务器初始化分成两部分的内容,第一部分在init/1 函数中,第二部分在handle_cast/2handle_info/2 中。这样做实际上只有一个原因,那就是如果初始化预计需要很长时间。然后将其拆分将允许gen_server:start_link 更快地返回,这对于主管启动的服务器很重要,因为它们在启动子进程时“挂起”,而一个启动缓慢的子进程可能会延迟整个主管启动。

    在这种情况下,我不认为拆分服务器初始化是不好的风格。

    小心错误很重要。 init/1 中的错误将导致主管终止,而第二部分中的错误将导致主管尝试重新启动该子项。

    我个人认为服务器向自己发送消息更好的样式,使用显式!gen_server:cast,就像使用良好的描述性消息一样,例如init_phase_2,这样会更容易看看发生了什么,而不是更匿名的超时。尤其是在其他地方也使用超时时。

    【讨论】:

    • 关于允许主管开始监控流程的好处。也许行为应该有一个second_init 回调开始? :-)
    【解决方案5】:

    坦率地说,我认为拆分初始化没有意义。在init 中进行繁重的工作确实会挂起主管,但使用timeout/handle_info,向self() 发送消息或将init_check 添加到每个处理程序(另一种可能性,虽然不是很方便)将有效地挂起调用进程。那么为什么我需要“工作”的主管和“不太工作”的 gen_server?干净的实现可能应该包括在初始化期间对任何消息的“not_ready”回复(为什么不从 init 产生完全初始化 + 完成后将消息发送回self(),这将重置“not_ready”状态),然后是“not_ready”回复应该由调用者正确处理,这增加了很多复杂性。只是暂停回复不是一个好主意。

    【讨论】:

    • 想象一个主管启动 100 个孩子,每个孩子做半秒的网络绑定初始化。来自主管的一分钟延迟是不好的,但客户端可能可以容忍来自 gen_server 的半秒延迟。
    • @cthulahoops 我可以想象(虽然不太明白这种连接的本质是什么,100 个听众?),但我相信半秒不是 CPU 时间。那么为什么不并行启动所有 100 个呢?这将是相同的半秒。
    • 目的是并行启动它们,但是在第一个从 init 返回之前,主管不会启动第二个孩子。因此,即使 gen_servers 会在短时间内阻塞请求,init 快速返回也很有用。
    • @cthulahoops 在主管的 init 中并行初始化所有 100 个连接,并将它们作为参数传递给子规范。我错过了什么?您甚至可以将子模块用于此类初始化以进行适当的封装。
    猜你喜欢
    • 2011-09-20
    • 2018-01-20
    • 2011-10-06
    • 1970-01-01
    • 2010-10-30
    • 1970-01-01
    • 2015-10-29
    • 2011-05-09
    • 1970-01-01
    相关资源
    最近更新 更多