6

我有一个gen_server在启动时尝试在监督树中的主管下启动一定数量的子进程(通常是 10-20)。gen_server 的 init 回调supervisor:start_child/2为每个需要的子进程调用。调用supervisor:start_child/2是同步的,因此在子进程启动之前它不会返回。所有子进程也是 gen_servers,因此 start_link 调用在 init 回调返回之前不会返回。在 init 回调中,调用了第三方系统,这可能需要一段时间才能响应(当调用第三方系统在 60 秒后超时时,我发现了这个问题)。同时 init 调用被阻塞了,这意味着supervisor:start_child/2也被阻塞了。所以整个调用的 gen_server 进程supervisor:start_child/2没有反应。在等待 start_child 函数返回时调用 gen_server 超时。因为这很容易持续 60 秒或更长时间。我想更改此设置,因为我的应用程序在等待时处于半启动状态。

解决此问题的最佳方法是什么?

我能想到的唯一解决方案是将与第三方系统交互的代码从 init 回调中移出到 handle_cast 回调中。这将使初始化回调更快。缺点是我需要gen_server:cast/2在所有子进程启动后调用。

有没有更好的方法来做到这一点?

4

1 回答 1

9

我见过的一种方法是使用 timeoutinit/1handle_info/2.

init(Args) ->
  {ok, {timeout_init, Args} = _State, 0 = _Timeout}.


...


handle_info( timeout, {timeout_init, Args}) ->
   %% do your inicialization
   {noreply, ActualServerState};  % this time no need for timeout 

handle_info( .... 

几乎所有结果都可以通过额外的超时参数返回,这基本上是等待另一条消息的时间。它给定时间流逝,handle_info/2被调用,具有timeout原子和服务器状态。在我们的例子中,超时等于 0,超时甚至应该在gen_server:start完成之前发生。这意味着handle_info甚至在我们能够将服务器的 pid 返回给其他任何人之前就应该调用它。所以这timeout_init应该是第一次调用我们的服务器,并给我们一些保证,我们在处理其他任何事情之前完成初始化。

如果您不喜欢这种方法(不是真的可读),您可以尝试向 self 发送消息init/1

init(Args) ->
   self() ! {finish_init, Args},
   {ok, no_state_yet}.

...


handle_info({finish_init, Args} = _Message, no_state_yet) ->
   %% finish whateva 
   {noreply, ActualServerState};

handle_info(  ... % other clauses 

同样,您要确保尽快将完成初始化的消息发送到该服务器,这对于在某个原子下注册的 gen_servers 非常重要。


编辑 在对 OTP 源代码进行了更仔细的研究之后。

当您通过它的 pid 与服务器通信时,这种方法已经足够好了。主要是因为 pid 在您的init/1函数返回后返回。gen_..但是在开始start/4start_link/4我们自动以相同名称注册进程的情况下略有不同。您可能会遇到一种竞争条件,我想更详细地解释一下。

如果进程是注册的,通常会简化所有调用并转换为服务器,例如:

count() ->
   gen_server:cast(?SERVER, count).

?SERVER通常模块名称(原子)在哪里,并且在这个名称下是一些注册的(和活动的)进程之前,它可以正常工作。当然,在底层,这cast是标准 Erlang 的消息发送!。它没有什么神奇之处,几乎和你在initwith中所做的一样self() ! {finish ...

但在我们的例子中,我们假设还有一件事。不仅仅是注册部分,还有我们的服务器完成了它的初始化。当然,由于我们正在处理消息框,因此需要多长时间并不重要,重要的是我们先收到哪条消息。所以确切地说,我们希望在接收finish_init消息之前接收count消息。

不幸的是,这种情况可能会发生。这是因为在调用回调之前注册gen了 OTP 中的 ' 。所以理论上,当一个进程调用将进入注册部分的函数时,另一个进程可以找到我们的服务器并发送消息,然后就可以使用消息调用该函数。机会很小(非常非常小),但它仍然可能发生。 init/1startcountinit/1finish_init

对此有三种解决方案。

首先是什么都不做。在这种竞争条件下,handle_cast将会失败(由于函数子句,因为我们的状态是not_state_yet原子的),并且主管将重新启动整个事情。

第二种情况是忽略这个坏消息/状态事件。这很容易实现

   ... ;
handle_cast( _, State) -> 
   {noreply, State}.

作为你的最后一个条款。不幸的是,大多数使用模板的人都使用这种不幸的(恕我直言)模式。

在这两种情况下,您可能会丢失一条count消息。如果这确实是一个问题,您仍然可以尝试通过将最后一个子句更改为来解决它

   ... ;
handle_cast(Message, no_state_yet) -> 
   gen_server:cast( ?SERVER, Message),
   {noreply, no_state_yet}.

但这还有其他明显的优势,我更喜欢“让它失败”的方法。

第三个选项是稍后注册过程。start/4与其使用并要求自动注册,不如使用start/3、接收 pid 并自己注册。

start(Args) ->
   {ok, Pid} = gen_server:start(?MODULE, Args, []),
   register(?SERVER, Pid),
   {ok, Pid}.

这样,我们finish_init在注册之前发送消息,并且在其他任何人可以发送消息之前发送count消息。

但是这种方法有其自身的缺点,主要是注册本身可能会以几种不同的方式失败。人们总是可以检查OTP 如何处理它,并复制此代码。但这是另一个故事。

所以最终这一切都取决于你需要什么,甚至你在生产中会遇到什么问题。重要的是要知道可能会发生什么坏事,但我个人不会尝试解决任何问题,直到我真正遭受这种竞争状况的影响。

于 2014-11-07T20:38:08.353 回答