超时

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

  1. receive
  2. Message1 [when Guard1] ->
  3. Actions1 ;
  4. Message2 [when Guard2] ->
  5. Actions2 ;
  6. ...
  7. after
  8. TimeOutExpr ->
  9. ActionsT
  10. end
TimeOutExpr是一个整数值表达式,表示毫秒数。时间的精确程度受到具体Erlang实现的底层操作系统以及硬件的限制——这是一个局部性问题(local issue)。如果在指定的时间内没有任何消息被匹配到,超时将会发生,ActionsT会被执行,而具体什么时候执行则是依赖与很多因素的,比如,和系统当前的负载有关系。例如,对于一个窗口系统,类似于下面的代码可能会出现在处理事件的进程中:
  1. 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毫秒:

  1. sleep(Time) ->
  2. receive
  3. after Time ->
  4. true
  5. end.

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

  1. flush_buffer() ->
  2. receive
  3. AnyMessage ->
  4. flush_buffer()
  5. after 0 ->
  6. true
  7. end.

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

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

  1. priority_receive() ->
  2. receive
  3. interrupt ->
  4. interrupt
  5. after 0 ->
  6. receive
  7. AnyMessage ->
  8. AnyMessage
  9. end
  10. end

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

程序 5.4

  1. -module(timer).
  2. -export([timeout/2,cancel/1,timer/3]).
  3.  
  4. timeout(Time, Alarm) ->
  5. spawn(timer, timer, [self(),Time,Alarm]).
  6.  
  7. cancel(Timer) ->
  8. Timer ! {self(),cancel}.
  9.  
  10. timer(Pid, Time, Alarm) ->
  11. receive
  12. {Pid,cancel} ->
  13. true
  14. after Time ->
  15. Pid ! Alarm
  16. end.

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

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