代码改变世界

F#中的异步及并行模式(3 - 下):代理的进一步使用

2010-03-21 18:19  Jeffrey Zhao  阅读(6165)  评论(17编辑  收藏  举报

在本系列的第3部分中,我们会来探索F#中轻量级的,交互式的代理,以及与代理有关的一些模式,包括隔离的内部状态。(译注:由于原文内容较多,译文拆成两段进行。在上半段文章中讨论了代理的基本使用方式,而下半段则讨论关于代理使用中更进一步的模式。)

消息与联合类型

很多时候我们会使用联合类型(Union Type)作为消息的类型。例如,我将要展示一个基于代理的DirectX示例,我们要在模拟引擎中使用如下的消息:

type Message =
    | PleaseTakeOneStep
    | PleaseAddOneBall of Ball 

模拟引擎中的代理:

let simulationEngine =
    Agent.Start(fun inbox ->
        async { while true do
                    // Wait for a message
                    let! msg = inbox.Receive() 

                    // Process a message
                    match msg with
                    | PleaseTakeOneStep -> state.Transform moveBalls
                    | PleaseAddOneBall ball -> state.AddObject ball  }) 

在很多情况下使用强类型消息是个不错的做法。不过,在某些您需要和其他消息机制协作的时候,也无需担心使用如“obj”和“string”等泛化的消息类型,此时代理只需要在运行时进行类型判断或转化即可。

参数化代理及抽象代理

代理只是F#编码中的一种设计模式。这意味着您可以将F#中各种常用的技巧,如参数化,抽象或是代码片段重用与代理一起使用。例如,您可以把之前的serveQuoteStream函数参数化,指定每条股票消息传输中的间隔时间:

open System.Net.Sockets 

/// serve up a stream of quotes
let serveQuoteStream (client: TcpClient, periodMilliseconds: int) = async {
    let stream = client.GetStream()
    while true do
        do! stream.AsyncWrite( "AAPL 439.2"B )
        do! Async.Sleep periodMilliseconds
} 

这意味着您的股票服务器中不同的请求可以拥有不同长度的间隔。

与此类似,您可以使用函数参数,将整个代理类的功能进行抽象:

let iteratingAgent job =
   Agent.Start(fun inbox ->
     async { while true do
               let! msg = inbox.Receive()
               do! job msg }) 

let foldingAgent job initialState =
   Agent.Start(fun inbox ->
     let rec loop state = async {
         let! msg = inbox.Receive()
         let! state = job state msg
         return! loop state
       }
     loop initialState)

您可以这样使用第一个函数:

let agent1 = iteratingAgent (fun msg -> async { do printfn "got message '%s'"  msg }) 

及第二个:

let agent2 =
    foldingAgent (fun state msg ->
        async { if state % 1000 = 0 then printfn "count = '%d'" msg;
                return state + 1 }) 0 

从代理返回结果

在以后的文章中,我们会讨论一些访问执行中的代理的部分结果的技巧,例如,我们可以使用每个MailboxProcessor代理的PostAndAsyncReply方法。这样的技巧在创建网络通信代理时显得尤其重要。

然而,这种做法很多时候有些过了,我们可能只是需要将结果汇报给一些如GUI般的监视环境。汇报部分结果的简单方法之一,便是之前在第二篇文章中讨论过的设计模式。下面便是这样一个例子,它创建了一个代理,对每1000条消息进行采样,并将得到的事件分发给GUI或其他管理线程(请注意,其中用到了第二篇文章中SynchronizationContext的两个扩展方法CaptureCurrent和RaiseEvent)。

// Receive messages and raise an event on each 1000th message 
type SamplingAgent() = 
    // The event that is raised 
    // Capture the synchronization context to allow us to raise events 
    // back on the GUI thread 
    let syncContext = SynchronizationContext.CaptureCurrent() 

    // The internal mailbox processor agent 
    let agent = 
        new MailboxProcessor<_>(fun inbox -> 
            async { let count = ref 0 
                    while true do 
                        let! msg = inbox.Receive() 
                        incr count 
                        if !count % 1000 = 0 then 
                            syncContext.RaiseEvent sample msg }) 

    /// Post a message to the agent 
    member x.Post msg = agent.Post msg 

    /// Start the agent 
    member x.Start () = agent.Start() 

    /// Raised every 1000'th message 
    member x.Sample = sample.Publish

您可以这样使用代理:

let agent = SamplingAgent() 

agent.Sample.Add (fun s -> printfn "sample: %s" s) 
agent.Start() 

for i = 0 to 10000 do 
   agent.Post (sprintf "message %d" i) 

与预料一致,这会报告agent的消息采样:

sample: message 999 
sample: message 1999 
sample: message 2999 
sample: message 3999 
sample: message 4999 
sample: message 5999 
sample: message 6999 
sample: message 7999 
sample: message 8999 
sample: message 9999

代理及错误

我们都无法避免错误和异常。良好的错误检测,报告及记录的措施是基于代理编程的基本要素。我们来看一下如何在F#的内存代理(MailboxProcessor)中检测和转发错误。

首先,F#异步代理的神奇之处在于异常可以由async { ... }自动捕获及分发,即使跨过多个异步等待及I/O操作。您也可以在async { ... }中使用try/with,try/finally及use关键字来捕获异常或释放资源。这意味着我们只需要在代理中处理那些未捕获的错误即可。

当MailboxProcessor代理中出现未捕获的异常时便会触发Error事件。一个常见的模式是将所有的错误转发给一个监视进程,例如:

type Agent<'T> = MailboxProcessor<'T> 

let supervisor = 
   Agent<System.Exception>.Start(fun inbox -> 
     async { while true do 
               let! err = inbox.Receive() 
               printfn "an error occurred in an agent: %A" err }) 

let agent = 
   new Agent<int>(fun inbox -> 
     async { while true do 
               let! msg = inbox.Receive() 
               if msg % 1000 = 0 then 
                   failwith "I don't like that cookie!" }) 

agent.Error.Add(fun error -> supervisor.Post error) 
agent.Start() 

我们也可以很方便地并行这些配置操作:

let agent = 
   new Agent<int>(fun inbox -> 
     async { while true do 
               let! msg = inbox.Receive() 
               if msg % 1000 = 0 then 
                   failwith "I don't like that cookie!" }) 
   |> Agent.reportErrorsTo supervisor 
   |> Agent.start 

或使用辅助模块:

module Agent = 
   let reportErrorsTo (supervisor: Agent<exn>) (agent: Agent<_>) = 
       agent.Error.Add(fun error -> supervisor.Post error); agent

   let start (agent: Agent<_>) = agent.Start(); agent 

下面是一个例子,我们创建了10000个代理,其中某些会报告错误:

let supervisor = 
   Agent<int * System.Exception>.Start(fun inbox -> 
     async { while true do 
               let! (agentId, err) = inbox.Receive() 
               printfn "an error '%s' occurred in agent %d" err.Message agentId }) 

let agents = 
   [ for agentId in 0 .. 10000 -> 
        let agent = 
            new Agent<string>(fun inbox -> 
               async { while true do 
                         let! msg = inbox.Receive() 
                         if msg.Contains("agent 99") then 
                             failwith "I don't like that cookie!" }) 
        agent.Error.Add(fun error -> supervisor.Post (agentId,error)) 
        agent.Start() 
        (agentId, agent) ]

我们发送消息:

for (agentId, agent) in agents do 
   agent.Post (sprintf "message to agent %d" agentId ) 

便可看到:

an error 'I don't like that cookie!' occurred in agent 99 
an error 'I don't like that cookie!' occurred in agent 991 
an error 'I don't like that cookie!' occurred in agent 992 
an error 'I don't like that cookie!' occurred in agent 993 
... 
an error 'I don't like that cookie!' occurred in agent 9999 

这一节我们处理了F#内存中的MailboxProcessor代理发生的错误。其他一些代理(例如,表示服务器端请求的代理)也可以这样进行设计与架构,以便进行优雅的错误转发及重试。

总结

隔离的代理是一种常用的编程模式,它不断运用在各种编程领域中,从设备驱动编程到用户界面,还包括分布式编程及高度伸缩的通信服务器。每次您编写了一个对象,线程或是异步工作程序,用于处理一个长时间的通信(如向声卡发送数据,从网络读取数据,或是响应一个输入的事件流),您其实就是在编写一种代理。每次您在写一个ASP.NET网页处理程序时,其实您也在使用一种形式的代理(每次调用时都重置状态)。在各种情况下,隔离与通信有关的状态是很常见的需求。

隔离的代理是一种最终的实现方式──例如,实现可伸缩的编程算法,包括可伸缩的请求服务器及分布式编程算法。与其他各种异步及并发编程模式一样,它们也不能被滥用。然而,他们是一种优雅、强大且高效的技术,使用非常广泛。

F#是一个独特的,随Visual Studio 2010一同出现的托管语言,完整支持轻量级的异步计算及内存种的代理。在F#中,异步代理可以通过组合的形式编写,而不用使用回调函数或控制反转等方式(译注:个人认为,事实上F#的这种做法其实是种优雅的控制反转)。这里有些权衡的地方──例如:在以后的文章中,我们会观察如何使用.NET类库中标准的APM模式来释放您的代理。然而,优势也是很明显的:易于控制,伸缩性强,并且在需要的时候,便可以在组织起CPU和I/O并行操作的同时,保持CPU密集型代码在.NET中的完整性能。

当然,也有其他一些.NET或基于JVM的语言支持轻量级的交互式代理──早前,有人认为这在.NET是“不可能”的事情,因为线程的代价十分昂贵。而如今,F#在2007年引入了“async { ... }”,这被视为语言设计上的一个突破──它让程序员可以在一个被业界广泛认可的编程平台上构建轻量级、组合式的异步编程及交互式的代理。除了Axum语言原型(它也受了F#的影响)之外,F#还证明了一个异步语言特性是一个完全可行的方法,这也解放了如今业界运行时系统设计领域的一个争论话题:我们是否要将线程做得轻量?

F#异步编程可以认为是一种“抢占”的实现,在此之前也有先驱的各种投入。例如OCaml delimited continuationsHaskell embedding of monadic concurrency及各种强调了并行中抢占的重要性的论文。

您可以在.NET 2.0、3.5及4.0、以及Linux/Mono/Mac、还有Silverlight中使用异步代理。此外,您甚至可以使用WebSharper平台将F#的异步编程模型翻译成JavaScript执行。好好享受吧!

原文:Async and Parallel Design Patterns in F#: Agents