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