【问题标题】:How to start a gen_server or gen_fsm on demand in Erlang without race conditions?如何在没有竞争条件的情况下在 Erlang 中按需启动 gen_server 或 gen_fsm?
【发布时间】:2016-05-17 11:38:49
【问题描述】:

我需要按需生成同一个 gen_fsm 的多个独立实例,然后能够将调用路由到正确的实例。

Gproc 库似乎是注册具有任意名称的进程的好方法。它有一个函数gproc:reg_or_locate/3 用于在没有竞争条件的情况下按需生成事物。这样我什至不需要主管——如果他们崩溃了,他们就会再次按需生成。但我不知道如何将gproc:reg_or_locate/3 应用于生成 gen_fsm 或 gen_server。

到目前为止我所做的尝试:

我只是通过那个函数调用 gen_server:start() ,它会创建一个中间进程,给中间进程命名,中间进程会产生一个 gen_server 并终止,我最终得到一个无名的 gen_server。

gen_server 和 gen_fsm 都导出一个 enter_loop 函数,如果我将它提供给 gproc:reg_or_locate/3,它似乎可以满足我的需要,但文档显示:

进程必须已使用以下启动函数之一启动 proc_lib,见proc_lib(3)

gproc:reg_or_locate/3 的文档没有提到他们通过 proc_lib 做任何事情。

或者,我可以让中间进程获取名称,然后自动将其传输到它产生的 gen_server 或 gen_fsm,但这会产生竞争条件:中间进程将具有 gen_fsm 的名称,并且任何用于 gen_fsm 的消息都会发送到中间过程就迷路了。

我觉得我在这里遗漏了一些简单的东西。这不是一个不常见的模式,所以应该有一个很好的方法来做到这一点。我错过了什么?

【问题讨论】:

    标签: multithreading erlang multiprocessing race-condition erlang-otp


    【解决方案1】:

    出于您的目的,我认为gproc:reg_or_locate/3 并没有真正为您提供任何有用的东西。如果它返回一个 PID(由于产生一个新进程或定位一个现有进程),该进程仍然可能在您向它发送消息之前死掉,所以除非您在基本 Erlang 消息传递之上有一个机制,否则您永远不会知道这没有发生。服务器也可能在收到消息之前死掉,或者即使在你发送消息时它还活着,它也可能在处理它时死掉,所以考虑到你对消息丢失表示担忧,解决方案的一个组成部分必须是可靠的消息机制。在您的情况下,明智且现成的解决方案是 gen_server:callgen_fsm:sync_send_event,而不仅仅是发送消息。

    这消除了消息从您希望实施的任何生成解决方案中丢失的问题。也就是说,您将知道消息丢失或失败,然后您可以采取任何适当的措施。

    现在,对于您的服务器的实际生成,总会存在一个竞争条件,即多个进程可能会尝试生成同一个服务器(具有给定名称的服务器),无论您如何实现它;在您执行任何其他操作之前,您要查找名称的任何内容(例如 erlang:whereis/1)都可能已过期(它可能会返回一个 PID,但 PID 可能会在您发送消息之前死掉,或者它可能会返回 undefined 但其他一些进程可以在您尝试之前注册名称),因此赢得(或输掉)比赛的唯一时间是在调用 erlang:register/2 时。

    那时您就知道可能会有一场比赛,但最多只能有一个获胜者。可能不是你,其他一些进程可能会打败你,但由于你命名你的进程并不重要,你可以简单地生成你的 gen_server,给它注册自己的名字,然后通过它发送消息名称:

    gen_server:start({local, Name}, ?MODULE, [], []),
    gen_server:call(Name, Message)
    

    谁赢得比赛并不重要(gen_server:start/4 调用可能会返回 {error,{already_started, Pid}}),但那又怎样,重要的是有人应该赢了,然后调用 gen_server:call 就有成功的机会.

    您显然确实需要确保调用返回了合适的成功结果,从技术上讲,您可以检查 noproc 异常并尝试再次生成它,但是您必须确保这不会成为无限循环。

    说实话,虽然你不关心监督,但我可能还是会监督它。在这种情况下,simple_one_to_one 主管的重启策略设置为temporary,因此它不会重生。然后你的服务器将被收集在一个地方,而不仅仅是漂浮在边缘,你会得到主管报告,这不是一件坏事。遗憾的是,您不会获得失控重启保护,因为这里没有重启,所以您仍然需要担心这一点(除非您将temporary 更改为transient)。然后,您的有效仲裁点将是 supervisor:start_child/2,您可以将所需的进程名称作为参数传递。

    【讨论】:

      【解决方案2】:

      按照迈克尔的建议,我也会去监督。

      您可以在gen_server:start_link 中使用{via,Module,ViaName} 来使用除atoms 之外的其他名称。详情见这里:http://erlang.org/doc/man/gen_server.html#start_link-4

      例如,gproc

      gen_server:start_link({via, gproc, {n, l, {?MODULE, Name}}, ?MODULE, [], []).
      

      在调用 gen_server 时不要忘记使用相同的 {via, gproc, ...} 结构,而不仅仅是使用 Name

      gen_server:call({via, gproc, {n, l, {?MODULE, Name}}, {execute_command, Command}). 
      

      我倾向于这样定义via

      -define(SERVER(Name), {via, gproc, {n, l, {?MODULE, Name}}}).
      

      然后像这样使用它:

      gen_server:start_link(?SERVER("Testing"), ?MODULE, [], []).
      gen_server:call(?SERVER("Testing"), {execute_command, Command}).
      

      然后您可以在具有 simple_one_for_one 策略和 temporary 子规范的主管中启动它,如下所示:

      主管

      -module(my_cool_sup).
      
      -behaviour(supervisor).
      
      %% API
      -export([start_link/1, start_child/1]).    
      %% Supervisor callbacks
      -export([init/1]).
      
      -define(SERVER, ?MODULE).
      
      %% Helper macro for declaring children of supervisor
      -define(CHILD(ChildName, Type, Args), {ChildName, {ChildName, start_link, Args}, temporary, 5000, Type, [ChildName]}).
      
      %%====================================================================
      %% API functions
      %%====================================================================
      
      start_link() ->
          supervisor:start_link({local, ?SERVER}, ?MODULE, []).
      
      start_child(Name) ->
          supervisor:start_child(?SERVER, [Name]).
      
      %%====================================================================
      %% Supervisor callbacks
      %%====================================================================
      
      init([]) ->
          RestartStrategy = {simple_one_for_one, 1, 5},
      
          Children = [?CHILD(my_cool_server, worker, [])],
      
          {ok, { RestartStrategy, Children} }.
      

      gen_server

      -module(my_cool_server).
      
      -behavior(gen_server).
      
      %% API
      -export([start_link/3, execute_command/3]).
      %% gen_server callbacks
      -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
           terminate/2, code_change/3]).
      
      -define(SERVER(Name), {via, gproc, {n, l, {?MODULE, Name}}}).
      
      %%%===================================================================
      %%% API
      %%%===================================================================
      
      start_link(Name) ->
          gen_server:start_link(?SERVER(Name), ?MODULE, [], []).
      
      execute_command(Name, Command) ->
          gen_server:call(?SERVER(Name), {execute_command, Command}). 
      
      %%%===================================================================
      %%% gen_server callbacks
      %%%===================================================================
      
      %% Your normal gen_server callbacks here...
      

      现在您可以使用my_cool_sup:start_child("My cool name"). 来启动您的子进程。他们将受到监督,如果他们已经开始,它将返回already_started,但不会引发错误。

      查看start_child了解更多详情:http://erlang.org/doc/man/supervisor.html#start_child-2

      【讨论】:

        猜你喜欢
        • 2013-12-23
        • 1970-01-01
        • 2015-11-20
        • 2023-02-09
        • 2013-07-16
        • 1970-01-01
        • 2014-03-26
        • 1970-01-01
        • 2021-02-14
        相关资源
        最近更新 更多