【问题标题】:Perform a synchronous operation on a dynamically generated gen_server在动态生成的 gen_server 上执行同步操作
【发布时间】:2014-07-25 15:26:01
【问题描述】:

我很难找到正确的方法来调用由具有simple_one_for_one 子策略的主管动态创建的 gen_server 实例。我正在尝试将数据访问控制创建为 gen_servers。每个实体都有自己的主管,该主管将根据需要创建 gen_server 实例,以便在数据库上实际执行 CRUD 操作。我了解定义子进程的过程,以及根据需要创建它们的过程。

最初,我的计划是将子创建过程抽象为 gen_server 模块中创建子的自定义函数,使用 gen_server:call() 在该子上触发请求的操作(例如查找、存储、删除),然后返回操作结果返回调用进程。但是,除非我弄错了,否则这将阻止任何其他尝试使用这些函数的进程,直到调用返回。这绝对不是我的想法。

我可能陷入了 OO 模式(我的背景是 Java),但似乎应该有一种干净的方法允许一个模块中的函数获取对子进程的引用,然后在没有泄露那个孩子的内脏。换句话说,我不想在实体主管上调用create_child() 方法,然后让我的应用程序代码针对该子PID(即gen_sever:call(Pid, {find_by_id, Id}))进行gen_server:calls。相反,我希望能够调用更像Child:find_by_id(Id) 的函数。

【问题讨论】:

  • 我不清楚为什么要有一个专门的中间进程来访问数据库。如果你只是在客户端进程中执行接口函数(例如Child:find_by_id(Id))有什么问题?当然在这种情况下 Child 不是适当的模块名称...
  • 基本思想是为任何可能需要一些时间的操作(例如数据库调用)分离一个进程。 AFAIK,让客户端代码同步调用 user_info:find_by_id(User),其中 user_info 是一个命名模块,会为应用程序造成瓶颈,特别是如果数据库调用在任何时间段内阻塞。在 gen_server 进程中封装数据库访问操作似乎是管理并发访问的合理方法。话虽如此,我是 Erlang 的新手,所以我可能完全脱离了基地。

标签: erlang erlang-otp gen-server


【解决方案1】:

完整的答案在很大程度上取决于您的应用程序——例如,一个gen_server 可能就足够了,或者您可能真的需要一个数据库连接池。但是您应该注意的一件事是,gen_server 可以在通过返回{noreply, NewState} 为客户端实际准备好回复之前从handle_call 回调返回,然后,一旦它准备好客户端回复,调用gen_server:reply/2 将其发送回客户端。这允许gen_server 为来自其他客户端的调用提供服务,而不会阻塞第一次调用。请注意,这要求gen_server 有一种方法可以将请求发送到数据库,而不必阻塞等待回复;这通常是通过让数据库发送在gen_server:handle_info/2 回调中到达的回复来实现的,将足够的信息传回gen_server 可以将数据库回复与正确的客户端请求相关联。另请注意,gen_server:call/2,3 的默认超时时间为 5 秒,因此如果您希望数据库调用的持续时间超过默认值,则需要处理此问题。

【讨论】:

    【解决方案2】:

    如果您看到的问题是您泄露 db-process 的内部实现是 gen_server,您可以实现 api,使其也将 pid 作为参数。

    -module(user).
    
    -behaviour(gen_server).
    
    -export([find_by_id/2]).
    
    find_by_id(Pid, Id) ->
        gen_server:call(Pid, {find_by_id, Id}).
    
    %% Lots of code omitted
    
    handle_call({find_by_id, Id}, From, State) ->
        ok.
    
    %% Lots more code omitted.
    

    这样您就不会告诉客户端该实现实际上是一个 gen_server(尽管有人也可以使用 gen_server:call)。

    【讨论】:

    • 但是在对 find_by_id 的调用返回之前,这不会阻塞用户模块吗?我实际上完全实现了这种模式,直到我开始考虑它在负载下会如何反应。我还没有到做批量测试的地步,真的不想错误地实现几十个实体。 :-(
    • 无法以这种格式输入我的cmets,看我的回答。
    【解决方案3】:

    当您创建、修改或删除记录时,您无需等待答复。您可以为此使用 gen_server:cast,但您不需要 gen_server,正如我在第一条评论中所说,对客户端进程中执行的接口函数的简单调用将节省时间。

    如果你想阅读,2种情况:

    • 您可以在等待应答的同时执行其他操作,然后调用 gen_server 即可,但一个简单的衍生进程等待应答并将其发送回客户端将提供相同的服务。

    • 在得到答案之前你不能做任何事情,那么就不存在阻塞问题,而且我认为最好使用尽可能少的代码,这样一个简单的函数调用就足够了。

    gen_server 旨在持久化并对消息做出反应。在您的示例中,我认为不需要持久化。

    -module(access).
    -export([add/2,get/1]).
    -record(foo, {bar, baz}).
    
    add(A,B) ->
      F = fun() ->
        mnesia:write(#foo{bar=A,baz=B})
      end,
      spawn(mnesia,activity,[transaction, F]). %% the function return immediately,
                                               %% but you will not know if the transaction failed
    
    get(Bar) ->
      F = fun() ->
        case mnesia:read({foo, Bar}) of
          [#foo{baz=Baz}] -> Baz;
          [] -> undefined
        end
      end,
      Pid = self(),
      Ref = make_ref(),
      Get = fun() ->
        R = mnesia:activity(transaction, F),
        Pid ! {Ref,baz,R}
      end,
      spawn(Get),
      Ref. %% the function return immediately a ref, and will send later the message {Ref,baz,Baz}.
    

    【讨论】:

    • 这很有意义。我猜是OTP近视。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-05-27
    • 1970-01-01
    • 2011-08-18
    • 2015-04-05
    • 1970-01-01
    • 1970-01-01
    • 2020-10-13
    相关资源
    最近更新 更多