F#中的异步及并行模式:代理的高级使用

开发 开发工具
F#是一个独特的函数式编程语言,完整支持轻量级的异步计算及内存种的代理。在F#中,异步代理可以通过组合的形式编写,而不用使用回调函数或控制反转等方式。

本文我们会来探索F#函数式编程语言的异步及并行模式、交互式的代理,以及与代理有关的一些模式,包括隔离的内部状态。

消息与联合类型

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

  1. type Message =  
  2.     | PleaseTakeOneStep  
  3.     | PleaseAddOneBall of Ball 模拟引擎中的代理:  
  4.  
  5. let simulationEngine =  
  6.     Agent.Start(fun inbox -> 
  7.         async { while true do  
  8.                     // Wait for a message  
  9.                     let! msg = inbox.Receive()  
  10.  
  11.                     // Process a message  
  12.                     match msg with  
  13.                     | PleaseTakeOneStep -> state.Transform moveBalls  
  14.                     | PleaseAddOneBall ball -> state.AddObject ball  }) 

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

参数化代理及抽象代理

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

  1. open System.Net.Sockets  
  2.  
  3. /// serve up a stream of quotes  
  4. let serveQuoteStream (client: TcpClient, periodMilliseconds: int) = async {  
  5.     let stream = client.GetStream()  
  6.     while true do  
  7.         do! stream.AsyncWrite( "AAPL 439.2"B )  
  8.         do! Async.Sleep periodMilliseconds  
  9. }  
  10.  
  11. 这意味着您的股票服务器中不同的请求可以拥有不同长度的间隔。与此类似,您可以使用函数参数,将整个代理类的功能进行抽象:  
  12.  
  13. let iteratingAgent job =  
  14.    Agent.Start(fun inbox -> 
  15.      async { while true do  
  16.                let! msg = inbox.Receive()  
  17.                do! job msg })  
  18.  
  19. let foldingAgent job initialState =  
  20.    Agent.Start(fun inbox -> 
  21.      let rec loop state = async {  
  22.          let! msg = inbox.Receive()  
  23.          let! state = job state msg  
  24.          return! loop state  
  25.        }  
  26.      loop initialState)您可以这样使用***个函数:  
  27.  
  28. let agent1 = iteratingAgent (fun msg -> async { do printfn "got message '%s'"  msg }) 及第二个:  
  29.  
  30. let agent2 =  
  31.     foldingAgent (fun state msg -> 
  32.         async { if state % 1000 = 0 then printfn "count = '%d'" msg;  
  33.                 return state + 1 }) 0 从代理返回结果 

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

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

  1. // Receive messages and raise an event on each 1000th message   
  2. type SamplingAgent() =   
  3.     // The event that is raised   
  4.     // Capture the synchronization context to allow us to raise events   
  5.     // back on the GUI thread   
  6.     let syncContext = SynchronizationContext.CaptureCurrent()  
  7.  
  8.     // The internal mailbox processor agent   
  9.     let agent =   
  10.         new MailboxProcessor<_>(fun inbox ->   
  11.             async { let count = ref 0   
  12.                     while true do   
  13.                         let! msg = inbox.Receive()   
  14.                         incr count   
  15.                         if !count % 1000 = 0 then   
  16.                             syncContext.RaiseEvent sample msg })  
  17.  
  18.     /// Post a message to the agent   
  19.     member x.Post msg = agent.Post msg  
  20.  
  21.     /// Start the agent   
  22.     member x.Start () = agent.Start()  
  23.  
  24.     /// Raised every 1000'th message   
  25.     member x.Sample = sample.Publish您可以这样使用代理:  
  26.  
  27. let agent = SamplingAgent()  
  28.  
  29. agent.Sample.Add (fun s -> printfn "sample: %s" s)   
  30. agent.Start()  
  31.  
  32. for i = 0 to 10000 do   
  33.    agent.Post (sprintf "message %d" i) 与预料一致,这会报告agent的消息采样:  
  34.  
  35. sample: message 999   
  36. sample: message 1999   
  37. sample: message 2999   
  38. sample: message 3999   
  39. sample: message 4999   
  40. sample: message 5999   
  41. sample: message 6999   
  42. sample: message 7999   
  43. sample: message 8999   
  44. sample: message 9999 

#p#
代理及错误

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

首先,F#异步代理的神奇之处在于异常可以由async { ... }自动捕获及分发,即使跨过多个异步等待及I/O操作。您也可以在async { ... }中使用try/with,try/finally及use关键字来捕获异常或释放资源。这意味着我们只需要在代理中处理那些未捕获的错误即可。当MailboxProcessor代理中出现未捕获的异常时便会触发Error事件。一个常见的模式是将所有的错误转发给一个监视进程,例如:

  1. type Agent<'T> = MailboxProcessor<'T> 
  2.  
  3. let supervisor =   
  4.    Agent<System.Exception>.Start(fun inbox ->   
  5.      async { while true do   
  6.                let! err = inbox.Receive()   
  7.                printfn "an error occurred in an agent: %A" err })  
  8.  
  9. let agent =   
  10.    new Agent<int>(fun inbox ->   
  11.      async { while true do   
  12.                let! msg = inbox.Receive()   
  13.                if msg % 1000 = 0 then   
  14.                    failwith "I don't like that cookie!" })  
  15.  
  16. agent.Error.Add(fun error -> supervisor.Post error)   
  17. agent.Start() 我们也可以很方便地并行这些配置操作:  
  18.  
  19. let agent =   
  20.    new Agent<int>(fun inbox ->   
  21.      async { while true do   
  22.                let! msg = inbox.Receive()   
  23.                if msg % 1000 = 0 then   
  24.                    failwith "I don't like that cookie!" })   
  25.    |> Agent.reportErrorsTo supervisor   
  26.    |> Agent.start 或使用辅助模块:  
  27.  
  28. module Agent =   
  29.    let reportErrorsTo (supervisor: Agent<exn>) (agent: Agent<_>) =   
  30.        agent.Error.Add(fun error -> supervisor.Post error); agent  
  31.  
  32.    let start (agent: Agent<_>) = agent.Start(); agent  

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

  1. let supervisor =   
  2.    Agent<int * System.Exception>.Start(fun inbox ->   
  3.      async { while true do   
  4.                let! (agentId, err) = inbox.Receive()   
  5.                printfn "an error '%s' occurred in agent %d" err.Message agentId })  
  6.  
  7. let agents =   
  8.    [ for agentId in 0 .. 10000 ->   
  9.         let agent =   
  10.             new Agent<string>(fun inbox ->   
  11.                async { while true do   
  12.                          let! msg = inbox.Receive()   
  13.                          if msg.Contains("agent 99") then   
  14.                              failwith "I don't like that cookie!" })   
  15.         agent.Error.Add(fun error -> supervisor.Post (agentId,error))   
  16.         agent.Start()   
  17.         (agentId, agent) ]我们发送消息:  
  18.  
  19. for (agentId, agent) in agents do   
  20.    agent.Post (sprintf "message to agent %d" agentId ) 便可看到:  
  21.  
  22. an error 'I don't like that cookie!' occurred in agent 99   
  23. an error 'I don't like that cookie!' occurred in agent 991   
  24. an error 'I don't like that cookie!' occurred in agent 992   
  25. an error 'I don't like that cookie!' occurred in agent 993   
  26. ...  
  27.  
  28. an error 'I don't like that cookie!' occurred in agent 999 

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

总结

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

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

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

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

文章转自老赵的博客,

原文地址:http://blog.zhaojie.me/2010/03/async-and-parallel-design-patterns-in-fsharp-3-more-agents.html

【编辑推荐】

  1. 详解F#异步及并行模式中的轻量级代理
  2. 详解F#异步及并行模式中的并行CPU及I/O计算
  3. TechED 09视频专访:F#与函数式编程语言
  4. F#中DSL原型设计:语法检查和语义分析
  5. 大话F#和C#:是否会重蹈C#失败的覆辙?

 

责任编辑:王晓东 来源: 博客
相关推荐

2010-03-16 09:09:04

F#

2010-03-26 19:03:19

F#异步并行模式

2010-03-08 09:17:13

F#异步

2010-03-26 19:22:08

F#代理

2010-04-07 16:51:59

F#

2010-04-06 15:20:56

ASP.NET MVC

2009-08-19 09:42:34

F#并行排序算法

2009-08-13 17:25:21

F#入门

2012-03-12 12:34:02

JavaF#

2010-01-26 08:25:06

F#语法F#教程

2012-04-10 10:04:26

并行编程

2009-11-16 09:05:46

CodeTimer

2010-01-07 10:04:18

F#函数式编程

2009-05-25 09:11:34

Visual StudF#微软

2010-01-15 08:33:13

F#F#类型推断F#教程

2011-03-23 10:40:51

java代理模式

2010-08-16 16:12:58

F#

2011-06-09 09:52:41

F#

2009-08-13 17:39:48

F#数据类型Discriminat

2024-03-28 12:51:00

Spring异步多线程
点赞
收藏

51CTO技术栈公众号