erlang:send_after/3 和 timer:send_after/3 是否旨在表现不同?

Are erlang:send_after/3 and timer:send_after/3 intended to behave differently?

我想在延迟后向进程发送消息,发现 erlang:send_after/4

在查看 docs 时,看起来这正是我想要的:

erlang:send_after(Time, Dest, Msg, Options) -> TimerRef

Starts a timer. When the timer expires, the message Msg is sent to the process identified by Dest.

但是,当目的地是 运行 在另一个节点上时,它似乎不起作用 - 它告诉我其中一个参数是错误的。

1> P = spawn('node@host', module, function, [Arg]).
<10585.83.0>
2> erlang:send_after(1000, P, {123}).
** exception error: bad argument
     in function  erlang:send_after/3
        called as erlang:send_after(1000,<10585.83.0>,{123})

timer:send_after/3 做同样的事情似乎工作正常:

1> P = spawn('node@host', module, function, [Arg]).
<10101.10.0>
2> timer:send_after(1000, P, {123}).
{ok,{-576458842589535,#Ref<0.1843049418.1937244161.31646>}}

而且,timer:send_after/3docs 状态几乎与 erlang 版本相同:

send_after(Time, Pid, Message) -> {ok, TRef} | {error, Reason}

Evaluates Pid ! Message after Time milliseconds.

所以问题是,为什么这两个表面上做同样事情的函数表现不同? erlang:send_after 是否已损坏或广告错误?或许 timer:send_after 没有按照我的想法行事?

TL;DR

您的假设是正确的:它们旨在做同样的事情,但实现方式不同。

讨论

timer 模块中的内容,例如 timer:send_after/2,3 通过将其定义为服务的 gen_server 工作。与任何其他服务一样,如果您为它分配了非常 大量 任务(要跟踪的计时器),则该服务可能会过载。

另一方面,

erlang:send_after/3,4 是直接在运行时内实现的 BIF,因此可以访问硬件计时器等系统原语。如果您有大量计时器,这绝对是您的不二之选。不过,在 大多数 程序中,您不会注意到差异。

Erlang Efficiency Guide:

中实际上有关于此的注释

3.1 Timer Module

Creating timers using erlang:send_after/3 and erlang:start_timer/3 , is much more efficient than using the timers provided by the timer module in STDLIB. The timer module uses a separate process to manage the timers. That process can easily become overloaded if many processes create and cancel timers frequently (especially when using the SMP emulator).

The functions in the timer module that do not manage timers (such as timer:tc/3 or timer:sleep/1), do not call the timer-server process and are therefore harmless.

解决方法

在没有相同节点限制的情况下获得 BIF 效率的解决方法是拥有一个您自己的进程,它只等待消息转发到另一个节点:

-module(foo_forward).
-export([send_after/3, cancel/1]).
% Obviously this is an example only. You would want to write this to
% be compliant with proc_lib, write a proper init/N and integrate with
% OTP. Note that this snippet is missing the OTP service functions.

start() ->
    spawn(fun() -> loop(self(), [], none) end).

send_after(Time, Dest, Message) ->
    erlang:send_after(Time, self(), {forward, Dest, Message}).

loop(Parent, Debug, State) ->
    receive
        {forward, Dest, Message} ->
            Dest ! Message,
            loop(Parent, Debug, State);
        {system, From, Request} ->
            sys:handle_msg(Request, From, Parent, ?MODULE, Debug, State);
        Unexpected ->
            ok = log(warning, "Received message: ~tp", [Unexpected]),
            loop(Parent, Debug, State)
    end.

上面的例子有点肤浅,但希望它表达了要点。应该可以获得 BIF erlang:send_after/3,4 的效率,但仍然设法跨节点发送消息,并让您可以自由地使用 erlang:cancel_timer/1

取消消息

但是为什么呢?

难题(和错误)是 erlang:send_after/3,4 不想跨节点工作的原因。您在上面提供的示例看起来有点奇怪,因为第一个分配给 P 的是 Pid <10101.10.0>,但崩溃的调用被报告为 <10585.83.0> - 显然不一样。

暂时不知道为什么erlang:send_after/3,4不行,但我可以肯定地说,两者之间的运作机制不是相同的。我会研究它,但我想 BIF 版本实际上在运行时做了一些有趣的事情来提高效率,结果通过 直接更新其邮箱 向目标进程发出信号在更高的 Erlang 到 Erlang 级别上实际发送 Erlang 消息。

也许我们两者都有很好,但是这绝对应该在文档中明确标记,显然不是(我刚刚检查过)。

如果您有多个计时器,超时顺序会有所不同。 下面的示例显示 erlang:send_after 不保证顺序,但是 计时器:send_after

1> A = lists:seq(1,10).
[1,2,3,4,5,6,7,8,9,10]
2> [erlang:send_after(100, self(), X) || X <- A].
...
3> flush().
Shell got 2
Shell got 3
Shell got 4
Shell got 5
Shell got 6
Shell got 7
Shell got 8
Shell got 9
Shell got 10
Shell got 1
ok
4> [timer:send_after(100, self(), X) || X <- A]. 
...
5> flush().
Shell got 1
Shell got 2
Shell got 3
Shell got 4
Shell got 5
Shell got 6
Shell got 7
Shell got 8
Shell got 9
Shell got 10
ok