18

Erlang 最开始吸引我的地方之一是 Actor 模型。不同进程同时运行并通过异步消息传递进行交互的想法。

我刚刚开始深入了解 OTP,尤其是查看 gen_server。我见过的所有示例 - 并且授予它们是教程类型的示例 - 使用handle_call()而不是handle_cast()实现模块行为。

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

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

我的问题是:

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

4 回答 4

24
  1. 取决于你的情况。

    如果你想得到一个结果,handle_call真的很常见。如果您对调用结果不感兴趣,请使用handle_cast. 使用handle_call时,调用者会阻塞,是的。这大部分时间都可以。让我们看一个例子。

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

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

  2. 使用handle_call将在通话期间阻塞进程。这将导致客户排队等待回复,因此整个事情将按顺序运行。

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

所以,总结一下:

  • usinghandle_call将阻塞调用者并在调用期间占用调用的进程。
  • 如果您希望并行活动继续进行,则必须并行化。做到这一点的唯一方法是启动更多进程,然后突然调用与强制转换不再是一个大问题(事实上,调用更舒服)。
于 2011-05-17T14:22:53.933 回答
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}).
于 2011-05-21T09:47:53.197 回答
1

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

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

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

于 2011-05-17T16:28:02.473 回答
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});

引用用于异步跟踪查询。

于 2013-05-28T02:14:10.320 回答