16.5.1 创建一个状态机处理器
我们前面创建的邮箱处理器是相当简单的,它能处理四种不同的消息,它保持一些本地状态,但它始终能够立即处理收到任何消息。情况不可能始终是这样的。如果一个邮箱处理器将一条消息发送到其他两个处理器,它可能需要在响应任何其他消息之前,收集这些处理器的回复。
我们可以写出表示状态机邮箱处理器,以类似于用异步工作流绘制矩形事件处理状态机的方式。完整的应用程序这是超出了本书的范围,但我们会创建一个简单的例子,显示所涉及的概念。
我们的消息处理器将存储一个整数,可以通过消息进行修改,有阻塞和恢复操作的能力。在阻塞期间,不再处理修改消息。为保持事情的简单,我们没有提供任何检索存储的整数的方法:当我们修改的状态时,将打印消息到控制台,因此,可以看到发生了什么。有头脑中有了这样的目标,就很容易设计出消息类型了:
type Message =
| ModifyState of int
| Block
| Resume
单个的消息是简单的,我们以前已经解释过 ModifyState 将做什么,其他两个消息是值得更详细地考虑。如果进程在初始状态,且接收了 Block,它停止处理所有 ModifyState 消息,并等待 Resume。当它处于阻止状态,发送给处理器的消息不会丢失:处理器有一个内部队列,用来保存消息,所以,一旦我们再次恢复它,就会处理阻塞期间接收到的所有消息。
清单 16.19 实现了这个邮箱处理器。我们将用递归的异步工作流函数,编码状态机,就像我们在绘图状态机中所做的一样:一个函数用于响应 active 状态的消息,另一个函数响应(或忽略)blocked 状态的消息。当邮箱从一个状态变到另一个状态时,两个函数彼此调用。
Listing 16.19 Mailbox processor using state machine (F#)
let mbox = MailboxProcessor.Start(fun mbox –>
let startTime = DateTime.UtcNow
let rec active(n) = async {
printfn "[%A] Processing: %d" (DateTime.UtcNow - startTime) n
let! msg = mbox.Receive()
match msg with
| ModifyState(by) -> return! active(n + by)
| Resume -> return! active(n)
| Block -> return! blocked(n) }
and blocked(n) =
printfn "[%A] Blocking" (DateTime.UtcNow - startTime)
mbox.Scan(fun msg –>
match msg with
| Resume -> Some(async {
let dt = (DateTime.UtcNow - startTime)
printfn "[%A] Resuming" dt
return! active(n) })
| _ -> None)
active(0) )
处理器首先调用 active 函数,用 0 作为初始状态。在此状态中,我们可以处理任何消息,所以,只需用 Receive 基元,它异步返回下一条消息。如果消息是 ModifyState,就更新数字,并继续处于 active 状态。在此状态下,Resume 没有太大的意义,(因为我们还没有收到 Block 的消息),所以,可以忽略它。当我们收到 Block 消息时,需要做一些事情,来停止处理 Resume 以外的所有消息,所以,我们调用 blocked 函数,它表示第二种状态。
当处理器处于阻止状态时,必须使用 Scan 基元,它能够指定我们可以处理哪些消息,哪些消息应保持在队列中稍后处理。Scan 成员取一个函数作参数,指定收到消息时做什么。在我们的示例中,当消息 Resume 时,返回一个异步工作流,Scan 成员将运行它。工作流将打印消息到控制台,并执行 active 函数,切换回活动状态。当处理器在阻塞状态中,接收到任何其他消息,Scan 执行这个 lambda 表达式,返回 None。这意味着,它不处理消息,所以,Scan 把这个消息添加到队列中,并等待另一个消息。
注意,当我们有多个线程发送 Block 和 Resume 消息时,我们实现的邮箱处理器不能很好地工作。当它已牌阻塞状态时,如果收到一个 Block 消息,不会处理这个消息,而是继续处理一次它第一个接收到的 Resume 消息。为了更强劲地解决这个问题,我们将不得不处理处于阻塞状态的 Block 消息,并递加一些数,表示阻止消息的数量。Resume 邮件会递减这个数,我们只在数字再次到达零以后,才恢复处理。这并不特别困难,但我们会保持示例代码的简单。