在 init 中向 self() 发送消息是否不好?

发布于 2024-11-08 15:36:43 字数 275 浏览 0 评论 0原文

这个示例中,作者通过执行以下操作来避免死锁情况:

自己() ! {start_worker_supervisor, Sup, MFA}

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

In this example, the author avoids a deadlock situation by doing:

self() ! {start_worker_supervisor, Sup, MFA}

in his gen_server's init function. I did something similar in one of my projects and was told this method was frowned upon, and that it was better to cause an immediate timeout instead. What is the accepted pattern?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

若言繁花未落 2024-11-15 15:36:43

Erlang 19+ 的更新

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

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

插入事件取代了调用您自己的状态处理函数的技巧,您通常必须在 gen_fsm 中使用这些函数来强制在其他事件之前处理插入的事件。

使用该模块中的操作功能,您可以确保生成事件在 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

Update for Erlang 19+

Consider using the new gen_statem behaviour. This behaviour supports generating of events internal to the FSM:

The state function can insert events using the action() next_event and such an event is inserted as the next to present to the state function. That is, as if it is the oldest incoming event. A dedicated event_type() internal can be used for such events making them impossible to mistake for external events.

Inserting an event replaces the trick of calling your own state handling functions that you often would have to resort to in, for example, gen_fsm to force processing an inserted event before others.

Using the action functionality in that module, you can ensure your event is generated in init and always handled before any external events, specifically by creating a next_event action in your init function.

Example:

...

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

...

Old answer

When designing a gen_server you generally have the choice to perform actions in three different states:

  • When starting up, in init/1
  • When running, in any handle_* function
  • When stopping, in terminate/2

A good rule of thumb is to execute things in the handling functions when acting upon an event (call, cast, message etc). The stuff that gets executed in init should not wait for events, that's what the handle callbacks are for.

So, in this particular case, a kind of "fake" event is generated. I'd say it seems that the gen_server always wants to initiate the starting of the supervisor. Why not just do it directly in init/1? Is there really a requirement to be able to handle another message in-between (the effect of doing it in handle_info/2 instead)? That windown is so incredibly small (the time between start of the gen_server and the sending of the message to self()) so it's highly unlikely to happen at all.

As for the deadlock, I would really advise against calling your own supervisor in your init function. That's just bad practice. A good design pattern for starting worker process would be one top level supervisor, with a manager and a worker supervisor beneath. The manager starts workers by calling the worker supervisor:

[top_sup]
  |    \
  |     \
  |      \
 man  [work_sup]
       /  |  \
      /   |   \
     /    |    \
    w1   ...   wN
滥情空心 2024-11-15 15:36:43

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

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

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

我个人认为服务器向自身发送消息是更好的风格,可以使用显式的 !gen_server:cast,就像良好的描述性消息一样,对于例如 init_phase_2,它会更容易看到发生了什么,而不是更匿名的超时。特别是如果其他地方也使用了超时。

Just to complement what has already been said about splitting a servers initialisation into two parts, the first in the init/1 function and the second in either handle_cast/2 or handle_info/2. There is really only one reason to do this and that is if the initialisation is expected to take a long time. Then splitting it up will allow the gen_server:start_link to return faster which can be important for servers started by supervisors as they "hang" while starting their children and one slow starting child can delay the whole supervisor startup.

In this case I don't think it is bad style to split the server initialisation.

It is important to be careful with errors. An error in init/1 will cause the supervisor to terminate while an error in the second part as they will cause the supervisor to try and restart that child.

I personally think it is better style for the server to send a message to itself, either with an explicit ! or a gen_server:cast, as with a good descriptive message, for example init_phase_2, it will be easier to see what is going on, rather than a more anonymous timeout. Especially if timeouts are used elsewhere as well.

°如果伤别离去 2024-11-15 15:36:43

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

init(...) ->
   gen_server:cast(self(), startup),
   {ok, ...}.

handle_cast(startup, State) ->
   slow_initialisation_task_reading_from_disk_fetching_data_from_network_etc(),
   {noreply, State}.

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

Calling your own supervisor sure does seem like a bad idea, but I do something similar all the time.

init(...) ->
   gen_server:cast(self(), startup),
   {ok, ...}.

handle_cast(startup, State) ->
   slow_initialisation_task_reading_from_disk_fetching_data_from_network_etc(),
   {noreply, State}.

I think this is clearer than using timeout and handle_info, it's pretty much guaranteed that no message can get ahead of the startup message (no one else has our pid until after we've sent that message), and it doesn't get in the way if I need to use timeouts for something else.

黑白记忆 2024-11-15 15:36:43

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

我认为最好的方法是使用状态机(gen_fsm)。我们的大多数 gen_srvs 实际上都是状态机,但是由于设置 get_fsm 的初始工作努力,我认为我们最终得到了 gen_srv。

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

我刚刚阅读了代码片段,但示例本身在某种程度上被破坏了——我不理解 gen_srv 操纵主管的这种构造。即使它是未来子进程池的管理者,这也是显式执行此操作的更重要原因,而无需依赖进程的邮箱魔法。在一些更大的系统中调试这也将是地狱。

This may be very efficient and simple solution, but I think it is not good erlang style.
I am using timer:apply_after, which is better and does not make impression of interacting with external module/gen_*.

I think that the best way would be to use state machines (gen_fsm). Most of our gen_srvers are really state machine, however because initial work effort to set up get_fsm I think we end up with gen_srv.

To conclude, I would use timer:apply_after to make code clear and efficient or gen_fsm to be pure Erlang style (even faster).

I have just read code snippets, but example itself is somehow broken -- I do not understand this construct of gen_srv manipulating supervisor. Even if it is manager of some pool of future children, this is even more important reason to do it explicitly, without counting on processes' mailbox magic. Debugging this would be also hell in some bigger system.

旧夏天 2024-11-15 15:36:43

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

Frankly, I don't see a point in splitting initialization. Doing heavy lifting in init does hang supervisor, but using timeout/handle_info, sending message to self() or adding init_check to every handler (another possibility, not very convenient though) will effectively hang calling processes. So why do I need "working" supervisor with "not quite working" gen_server? Clean implementation should probably include "not_ready" reply for any message during initialization (why not to spawn full initialization from init + send message back to self() when complete, which would reset "not_ready" status), but then "not ready" reply should be properly processed by the caller and this adds a lot of complexity. Just suspending a reply is not a good idea.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文