如何在没有竞争条件的情况下在 Erlang 中按需启动 gen_server 或 gen_fsm?

Shn*_*sel 5 erlang multithreading erlang-otp multiprocessing race-condition

我需要根据需要生成同一个 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 的消息都将转到中间进程过程并迷路。

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

Mic*_*ael 3

就您的目的而言,我认为并没有gproc:reg_or_locate/3真正给您带来任何有用的东西。如果它返回一个 PID(由于生成一个新进程,或定位一个现有进程),该进程仍然可能在您向其发送消息之前死亡,因此除非您有一种基于基本 Erlang 消息传递的机制,否则您永远不会知道这并没有发生。服务器也可能在接收消息之前死亡,或者即使在发送消息时服务器还活着,也可能在处理消息之前死亡,因此,考虑到您对消息丢失表示担忧,解决方案的一个组成部分必须是可靠的消息机制。gen_server:call对于您的情况,明智且现成的解决方案是gen_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)
Run Code Online (Sandbox Code Playgroud)

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

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

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