Erlang / OTP:同步与异步消息传递

问题描述 投票:16回答:4

首先吸引我到Erlang的一件事是Actor模型;不同进程同时运行并通过异步消息交互的想法。

我刚刚开始涉足OTP,尤其是研究gen_server。我所见过的所有示例(当然都是教程类型的示例)都使用handle_call()而不是handle_cast()来实现模块行为。

我发现这有点令人困惑。据我所知,handle_call是一个同步操作:呼叫者被阻止,直到被呼叫者完成并返回为止。这似乎与异步消息传递的原理背道而驰。

我将要启动一个新的OTP应用程序。这似乎是一项基本的体系结构决策,因此在开始之前,我想确保自己理解。

我的问题是:

  1. 实际上,人们倾向于使用handle_call而不是handle_cast
  2. 如果是这样,当多个客户端可以调用同一进程/模块时,对可伸缩性有什么影响?
erlang otp
4个回答
23
投票
  1. 取决于您的情况。

    如果要获得结果,handle_call确实很常见。如果您对通话结果不感兴趣,请使用handle_cast。使用handle_call时,呼叫者会阻止,是的。大部分时间都可以。让我们看一个例子。

    [如果您有一个Web服务器,该服务器将文件的内容返回给客户端,则可以处理多个客户端。每个客户端have等待读取文件的内容,因此在这种情况下使用handle_call会很好(除了愚蠢的示例)。

    [当您确实需要发送请求,进行其他处理然后在以后获取答复时,通常会使用两次调用(例如,一次强制转换和一次调用以获取结果)或正常的消息传递。但这是一种相当罕见的情况。

  2. 使用handle_call将在通话期间阻止该过程。这将导致客户排队等待他们的答复,因此整个过程将按顺序运行。

    如果要并行代码,则必须编写并行代码。唯一的方法是运行多个进程。

所以,总结一下:

  • 使用handle_call将阻止呼叫者并占用呼叫持续时间内被呼叫的过程。
  • 如果要继续进行并行活动,则必须并行化。唯一的方法是启动更多的进程,突然之间,调用vs强制转换不再是一个大问题了(实际上,调用更加舒适)。

11
投票

亚当的答案很好,但我要补充一点

使用handle_call将在整个呼叫过程中阻止该过程。

这始终是真实的对于进行handle_call呼叫的客户端。这花了我一段时间,但并不一定意味着gen_server在回答handle_call时也必须阻塞。

[在我的情况下,当我创建一个处理gen_server的数据库并故意编写一个执行SELECT pg_sleep(10)的查询时遇到了这个问题,这是PostgreSQL所说的“睡眠10秒”,这是我测试非常昂贵的查询的方式。我的挑战:我不希望数据库gen_server坐在那里等待数据库完成!

我的解决方法是使用gen_server:reply/2

当无法在Module:handle_call / 3的返回值中定义答复时,gen_server可以使用此函数将答复显式发送给名为call / 2,3或multi_call / 2,3,4的客户端。 。

使用代码:

-module(database_server).
-behaviour(gen_server).
-define(DB_TIMEOUT, 30000).

<snip>

get_very_expensive_document(DocumentId) ->
    gen_server:call(?MODULE, {get_very_expensive_document, DocumentId}, ?DB_TIMEOUT).    

<snip>

handle_call({get_very_expensive_document, DocumentId}, From, State) ->     
    %% Spawn a new process to perform the query.  Give it From,
    %% which is the PID of the caller.
    proc_lib:spawn_link(?MODULE, query_get_very_expensive_document, [From, DocumentId]),    

    %% This gen_server process couldn't care less about the query
    %% any more!  It's up to the spawned process now.
    {noreply, State};        

<snip>

query_get_very_expensive_document(From, DocumentId) ->
    %% Reference: http://www.erlang.org/doc/man/proc_lib.html#init_ack-1
    proc_lib:init_ack(ok),

    Result = query(pgsql_pool, "SELECT pg_sleep(10);", []),
    gen_server:reply(From, {return_query, ok, Result}).

1
投票

IMO,在并行世界中,handle_call通常不是一个好主意。假设我们有进程A(gen_server)接收到某个事件(用户按下了按钮),然后将消息投射到进程B(gen_server),请求对该按钮进行大量处理。进程B可以生成子进程C,该子进程C在准备就绪时将消息投射回A(然后是将消息投射到B,然后将消息投射到A)。在处理期间,A和B都准备接受新请求。当A收到来自C(或B)的广播消息时,例如向用户显示结果。当然,第二个按钮可能会在第一个按钮之前被处理,因此A应该以正确的顺序累积结果。通过handle_call阻塞A和B将使该系统成为单线程(尽管将解决订购问题)

实际上,生成C类似于handle_call,不同之处在于C是高度专业化的,仅处理“一条消息”并在此之后退出。 B应该具有其他功能(例如,限制工作人员数,控制超时),否则可以从A派生C。

编辑:C也是异步的,因此生成C时它与handle_call不相似(B没有被阻止)。


0
投票

有两种方法。一种是更改为使用事件管理方法。我正在使用的是如图所示使用演员表...

    submit(ResourceId,Query) ->
      %%
      %% non blocking query submission
      %%
      Ref = make_ref(),
      From = {self(),Ref},
      gen_server:cast(ResourceId,{submit,From,Query}),
      {ok,Ref}.

投射/提交代码为...

    handle_cast({submit,{Pid,Ref},Query},State) ->
      Result = process_query(Query,State),
      gen_server:cast(Pid,{query_result,Ref,Result});

该参考用于异步跟踪查询。

© www.soinside.com 2019 - 2024. All rights reserved.