OTP - 同步和异步消息传递

时间:2011-05-17 13:58:17

标签: erlang

免责声明:erlang newbie。

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

我刚刚开始关注OTP,特别是关注gen_server。我见过的所有示例 - 并且授予它们是教程类型示例 - 使用handle_call()而不是handle_cast()来实现模块行为。

我觉得有点混乱。据我所知,handle_call是一个同步操作:调用者被阻塞,直到被调用者完成并返回。这似乎与异步消息传递哲学背道而驰。

我即将开始一个新的OTP应用程序。这似乎是一个基本的架构决策,所以我想在开始之前确定我理解。

具体来说,我的问题是:

  1. 在实际操作中,人们倾向于使用handle_call而不是handle_cast吗?
  2. 如果是这样,当多个客户端可以调用相同的进程/模块时,可扩展性的影响是什么?
  3. 感谢。

4 个答案:

答案 0 :(得分:23)

  1. 取决于您的情况。

    如果您想获得结果,handle_call确实很常见。如果您对通话结果不感兴趣,请使用handle_cast。使用handle_call时,调用者将阻止,是的。这大部分时间还可以。我们来看一个例子。

    如果您有一个Web服务器,它将文件内容返回给客户端,您将能够处理多个客户端。每个客户端都有来等待读取文件的内容,因此在这种情况下使用handle_call会非常好(除了愚蠢的例子)。

    当您确实需要发送请求,执行其他处理然后稍后获得回复的行为时,通常会使用两个调用(例如,一个强制转换和一个调用来获取结果)或正常的消息传递。但这是一个相当罕见的案例。

  2. 使用handle_call将阻止呼叫持续时间的过程。这将导致客户排队获得他们的回复,因此整个过程将按顺序运行。

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

  3. 所以,总结一下:

    • 使用handle_call将阻止呼叫者并占用呼叫期间呼叫的进程。
    • 如果您希望继续进行并行活动,则必须进行并行化。唯一的方法是启动更多进程,突然调用vs cast不再是一个大问题(事实上,它更适合调用)。

答案 1 :(得分:11)

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

  
    

使用handle_call将阻止呼叫持续时间的过程。

  

对于发出handle_call调用的客户,这始终是。我花了一些时间来解决这个问题,但这并不一定意味着gen_server在回答handle_call时也必须阻止。

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

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

  
    

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

  

在代码中:

-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}).

答案 2 :(得分: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应该具有其他功能(例如限制工作人员数量,控制超时),否则C可以从A中生成。

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

答案 3 :(得分: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});

该引用用于异步跟踪查询。