16.4.2 创建邮箱处理器
让我们看看如何在实践中使用消息传递。我们会扩展前一节的应用程序,添加一个进程,保存应用程序的当前状态:当前选定的颜色(我们会添加一个选项来改变颜色),和到目前为止绘制的所有矩形的列表。应用程序处理的消息,可能来自绘制矩形的进程,也可能来自我们添加到应用程序中的其他事件处理程序所发送的。在这一节的介绍中,我们提到过这个演示的扩展版本,邮件也可以从处理网络通讯组件并发地发送。在 F# 中,可以接收消息的进程,也称为邮箱处理器(mailbox processors)。在实现邮箱处理器之前,我们需要知道消息是什么。
实现消息类型
每个进程可以处理一个已知类型的消息,因此,我们首先声明一个类型,来表示消息。我们将使用差别联合,因为这样,我们可以用一个联合类型的识别器,表示多种类型的消息。在清单 16.12 中,你可以看到,为此目的,差别联合是正确的 F# 类型。
Listing 16.12 The type representing messages (F#)
type RectData = Color * (int * int) * (int * int)
type DrawingMessage =
| AddRectangle of RectData
| SetColor of Color
| GetRectangles of AsyncReplyChannel<list<RectData>>
| GetColor of AsyncReplyChannel<Color>
清单 16.12 首先声明一个类型别名,RectData,这是一个元组,包含了存储一个矩形需要的所有信息。这个差别联合本身则包含两种类型的消息。前两个消息用来更改当前的状态。在 AddRectangle 情况下,处理器把新创建矩形的有关信息添加到一个内部列表;在 SetColor 情况下,将更改当前选定的颜色。
接下来的两个消息看起来有点棘手,因为它们所携带值的类型是 AsyncReplyChannel<'T>。这种类型创建的消息,可以发送回复给调用方。在我们的例子中,这意味着,当进程接收这些消息中的一个,将发送一个回复,既可以是包含所有矩形的列表,也可以是当前所选的颜色。
现在,我们已经得到了这个消息,下面就需要知道如何实现邮箱处理器,以及如何发送和接收消息。让我们从邮箱处理器开始。
实现处理器
一般情况下,邮箱处理器可以相当复杂,它们可以执行计算,以响应接收到的消息;还可以将消息发送给其他处理器,并收集回复;甚至还能启动新的邮箱处理器。我们的示例很简单:邮箱处理器保存应用程序的当前状态,处理读取或更新这个状态的消息。
清单 16.13 实现处理器的方式,与我们以前的状态机代码完全相同。大多数代码是使用异步工作流写的递归函数,用函数参数来维护当前状态。
Listing 16.13 Creating the mailbox processor (F#)
let state = MailboxProcessor.Start(fun mbox �C>
let rec loop(clr, rects) = async {
let! msg = mbox.Receive()
match msg with
| SetColor(newClr) �C>
return! loop(newClr, rects)
| AddRectangle(newRc) �C>
form.Invalidate()
return! loop(clr, rects@[newRc])
| GetColor(chnl) �C>
chnl.Reply(clr)
return! loop(clr, rects)
| GetRectangles(chnl) �C>
chnl.Reply(rects)
return! loop(clr, rects) }
loop(Color.IndianRed, []) )
为了创建邮箱处理器,我们使用 MailboxProcessor 类型的 Start 会员。它初始化邮件的消息,并运行到指定的函数,启动处理消息。这个函数将返回一个异步工作流,可以使用我们在初始化过程中作为参数得到的邮箱的 Receive 方法,等待消息。
我们实现工作流使用的递归函数,叫 loop,有两个参数。参数 clr 表示当前选定的颜色,rects 是一个矩形的列表。要从 lambda 函数返回工作流,我们用红色和一个空列表初始状态,来调用 loop。
现在,让我们看一下 loop 的函数体。它首先从邮箱中接收下一条消息。邮箱内部以队列存储,因此,如果一条消息已经在队列中,它会立即返回。如果队列为空,Receive 方法会阻塞这个工作流(没有阻塞实际的线程),一旦消息发送到处理器,就恢复。我们接收到一条消息,就使用模式匹配来决定如何处理它。前两个消息修改了处理器的状态,所以,我们用更新后的状态递归调用 loop 函数,使用 return! 关键字。注意,当我们得到一个新的矩形时,要将其加到列表的末尾,保证它将显示在前面,所以,我们使用 @ 运算符连接列表。
后两个消息用于读取处理器的状态,并携带回复通道作为参数。当处理器收到消息时,使用通道的 Reply 方法,结果发送矩形列表,或当前颜色给调用方,然后,再循环,并不更改状态的结果。
注意
当写邮箱处理器时,很重要的一点,是理解考虑到线程,它们是如何执行的。正在执行的线程,在等待异步工作流时,可以更改主体,但是,一个邮箱处理器实例的主体将永远不会同时运行在多个线程上。如果收到新邮件,正是我们在处理一个已有的,那它只就排队,稍后处理。我们的示例不执行任何复杂的计算,因此它几乎总是立即处理消息。邮箱处理器是按照线程安全设计的,所以,当我们用它们来存储并发访问的状态时,不必担心竞争条件。
现在,我们已准备好了邮箱处理器,再看我们需要怎样的改变,就能使用和更新存储在处理器中的状态了。