超时

超时

Erlang中用于接收消息的基本原语receive可以通过添加一个可选的超时子句来进行增强,完整的语法变成这样:

receive
    Message1 [when Guard1] ->
        Actions1 ;
    Message2 [when Guard2] ->
        Actions2 ;
    ...
after
    TimeOutExpr ->
        ActionsT
end
``` `TimeOutExpr`是一个整数值表达式,表示**毫秒** 数。时间的精确程度受到具体Erlang实现的底层操作系统以及硬件的限制——这是一个局部性问题(local issue)。如果在指定的时间内没有任何消息被匹配到,超时将会发生,`ActionsT`会被执行,而具体什么时候执行则是依赖与很多因素的,比如,和系统当前的负载有关系。例如,对于一个窗口系统,类似于下面的代码可能会出现在处理事件的进程中: ```
get_event() ->    receive        {mouse, click} ->            receive                {mouse, click} ->                    double_click            after double_click_interval() ->                single_click            end        …    end.
``` 在这个模型中,事件由消息来表示。`get_event`函数会等待一个消息,然后返回一个表示对应事件的原子式。我们希望能检测鼠标双击,亦即在某一个较短时间段内的连续两次鼠标点击。当接收到一个鼠标点击事件时我们再通过`receive`试图接收下一个鼠标点击事件。不过,我们为这个`receive`添加了一个超时,如果在指定的时间内(由`double_click_interval`指定)没有发生下一次鼠标点击事件,`receive`就会超时,此时`get_event`会返回`single_click`。如果第二个鼠标点击事件在给定的超时时限之内被接收到了,那么`get_event`将会返回`double_click`。在超时表达式的参数中有两个值有特殊意义:`infinity`

> 原子式`infinity`表示超时**永远** 也不会发生。如果超时时间需要在运行时计算的话,这个功能就很有用。我们可能会希望通过对一个表达式进行求值来得到超时长度:如果返回值是`infinity`的话,则永久等待。

`0`

> 数值0表示超时会立即发生,不过在那之前系统仍然会首先尝试对邮箱中已有的消息进行匹配。

在`receive`中使用超时比一下子想象到的要有用得多。函数`sleep(Time)`将当前进程挂起`Time`毫秒:

sleep(Time) -> receive after Time -> true end.


`flush_buffer()`清空当前进程的邮箱:

flush_buffer() -> receive AnyMessage -> flush_buffer() after 0 -> true end.


只要邮箱中还有消息,第一个消息会被匹配到(未绑定变量`AnyMessage`会匹配到任何消息,在这里就是第一个消息),然后`flush_buffer`会再次被调用,但是如果邮箱已经为空了,那么函数会从超时子句中返回。

消息的优先级也可以通过使用0作为超时长度来实现:

priority_receive() -> receive interrupt -> interrupt after 0 -> receive AnyMessage -> AnyMessage end end


函数`priority_receive`会返回邮箱中第一个消息,**除非** 有消息`interrupt`发送到了邮箱中,此时将返回`interrupt`。通过首先使用超时时长0来调用`receive`去匹配`interrupt`,我们可以检查邮箱中是否已经有了这个消息。如果是,我们就返回它,否则,我们再通过模式`AnyMessage`去调用`receive`,这将选中邮箱中的第一条消息。

程序 5.4

-module(timer). -export([timeout/2,cancel/1,timer/3]).

timeout(Time, Alarm) -> spawn(timer, timer, [self(),Time,Alarm]).

cancel(Timer) -> Timer ! {self(),cancel}.

timer(Pid, Time, Alarm) -> receive {Pid,cancel} -> true after Time -> Pid ! Alarm end. ```

在receive中的超时纯粹是在receive语句内部的,不过,要创建一个全局的超时机制也很容易。在程序5.4中的timer模块中的timer::timeout(Time,Alarm)函数就实现了这个功能。

调用timer:timeout(Time,Alarm)会导致消息Alarm在时间Time之后被发送到调用进程。该函数返回计时器进程的标识符。当进程完成自己的任务之后,可以使用该计时器进程标识符来等待这个消息。通过调用timer::cancel(Timer),进程也可以使用这个标识符来撤销计时器。需要注意的是,调用timer:cancel并不能保证 调用进程不会收到Alarm消息,这是由于cancel消息有可能在Alarm消息被发送出去之后才被收到的。