F#中的异步和并行设计模式(二):用事件触发来报告进度

11 篇文章 0 订阅
4 篇文章 0 订阅

在这篇文章中,我们将着眼于一个常用的异步设计模式,我叫它用事件触发来报告进度。在这篇文章后面,我们将使用这种设计模式从推特上抽样读取帖子流。

这是F#异步编程基础技术系列的第二部分,这里有些例子的代码是摘自F# JAOO 教程

·        第一部分描述了F# 通过支持轻量级交互是一种怎样的并行和交互式语言,并且介绍了 并行CPU异步处理和并行I/O异步处理模式。

·        第二部分就是这篇文章。

·        第三部分描述了F# 中的轻量级代理、交互式代理、隔离式代理。

模式3:用事件触发来报告进度

让我们先来看看设计模式的本质的一个实例。下面,我们定义一个对象来协调一组asyncs的并行执行。每个工作在它结束时就报告结果,而不是等待结果集。

下面用黄色高亮将设计模式的本质标记出来了:

·        当前“同步的内容”是从对象开始方法的GUI线程抓取的。这是个句柄,它能让我在GUI上下文中运行代码及触发事件。这里定义了一个私有帮助函数来触发任意的F# 事件。严格来说,这不是必须的,但可以让代码更整洁。

·        定义一个或多个事件。这些事件以属性形式发布,如果这个对象将被.Net的其他语言引用的话就标记[<CLIEvent>]。

·        在有后台工作被启动的情况下,通过指定一个异步工作流来定义执行后台工作。Async.Start启动一个工作流的实例(当然Async.StartWithContinuations也会经常被作为替换来使用,本文下面的例子会用到)。这些事件会在后台工作执行的进展中适时地被触发。

下面是方法的示例在这篇文章中,我们将着眼于一个常用的异步设计模式,我叫它用事件触发来报告进度。在这篇文章后面,我们将使用这种设计模式从推特上抽样读取帖子流.

type AsyncWorker<'T>(jobs: seq<Async<'T>>) =   
    // This declares an F# event that we can raise
    let jobCompleted = new Event<int * 'T>()

    /// Start an instance of the work
    member x.Start()   =
        // Capture the synchronization context to allow us to raise events back on the GUI thread
        let syncContext = SynchronizationContext.CaptureCurrent()
 
        // Mark up the jobs with numbers
        let jobs = jobs |> Seq.mapi (fun i job-> (job,i+1))  
        let work = 
            Async.Parallel
               [ for (job,jobNumber) in jobs->
                  async { let! result = job
                          syncContext.RaiseEvent jobCompleted (jobNumber,result)
                          return result } ]
 
        Async.Start(work |> Async.Ignore)

    /// Raised when a particular job completes
    member x.JobCompleted = jobCompleted.Publish

这里的代码使用两个在System.Threading.SynchronizationContext命名空间的帮助扩展方法,在这系列的文章中,我们会经常使用到。下面是方法的示例:

type SynchronizationContextwith
    /// A standard helper extension method to raise an event on the GUI thread
    member syncContext.RaiseEvent (event: Event<_>) args =
        syncContext.Post((fun _-> event.Trigger args),state=null)
 
    /// A standard helper extension method to capture the current synchronization context.
    /// If none is present, use a context that executes work in the thread pool.
    staticmember CaptureCurrent () =
        match SynchronizationContext.Currentwith
        |null -> new SynchronizationContext()
        | ctxt-> ctxt
现在你能用这个组件来监督CPU密集的异步线程集合的执行:

    letrec fib i = if i <2 then 1 else fib (i-1) + fib (i-2)    
    let worker =
        new AsyncWorker<_>( [ for i in 1 .. 100-> async { return fib (i %40) } ] )
 
    worker.JobCompleted.Add(fun (jobNumber, result)->
        printfn "job %d completed with result %A" jobNumber result)

    worker.Start()  
当运行时,每个工作完成后,程序都会报告进度: 
job 1 completed with result 1
job 2 completed with result 2
...
job 39 completed with result 102334155
job 77 completed with result 39088169
job 79 completed with result 102334155
当然从后台进程获取结果报告有许多方法。 百分之九十的情况下,最简单的方法就如上面展示的那样:通过在GUI(或者ASP.NET页面加载)线程上触发.NET事件来报告结果。 这个技术完全隐藏了后台线程的使用,采用完全标准的任何.NET程序员都熟悉的.NET习俗。这确保了用来实现你并行编程的这项技术被适当的封装了。
报告I/O异步处理的进度
用事件触发来报告进度模式当然也能同I/O异步一同使用。例如,下面一系列的I/O任务:

    open System.IO
    open System.Net
    open Microsoft.FSharp.Control.WebExtensions  
    /// Fetch the contents of a web page, asynchronously.
    let httpAsync(url:string) =
        async { let req = WebRequest.Create(url)
                use! resp = req.AsyncGetResponse()
                use stream = resp.GetResponseStream()
                use reader = new StreamReader(stream)
                let text = reader.ReadToEnd()
                return text }

    let urls =
        [ "http://www.live.com";
          "http://news.live.com";
          "http://www.yahoo.com";
          "http://news.yahoo.com";
          "http://www.google.com";
          "http://news.google.com"; ]
 
    let jobs =  [for url in urls -> httpAsync url ]
   
    let worker =new AsyncWorker<_>(jobs)
    worker.JobCompleted.Add(fun (jobNumber, result)->
        printfn "job %d completed with result %A" jobNumber result.Length)
 
    worker.Start()
当代码运行,会逐步地报告结果,显示每个网页的长度: 
job 5 completed with result 8521
job 6 completed with result 155767
job 3 completed with result 117778
job 1 completed with result 16490
job 4 completed with result 175186
job 2 completed with result 70362
一些工作会报告多种不同的事件
在这种设计模式中,我们用一个对象来封装和监控并行组合的异步线程的执行,其中的一个理由是它让通过进一步的事件来丰富(有更多)监督进度的API变得更简单。例如, 下面代码会在所有工作都完成以后,或者在任何一个工作中检测到错误时,或者在整个程序完成前被成功取消时触发额外的事件。下面高亮部分显示了事件的定义、事件的触发和事件的发布。 
open System
open System.Threading
open System.IO
open Microsoft.FSharp.Control.WebExtensions
 
type AsyncWorker<'T>(jobs: seq<Async<'T>>) =
     // Each of these lines declares an F# event that we can raise
    let allCompleted  = new Event<'T[]>()
    let error         = new Event<System.Exception>()
    let canceled      = new Event<System.OperationCanceledException>()
    let jobCompleted  = new Event<int * 'T>()
 
    let cancellationCapability =new CancellationTokenSource()
 
    /// Start an instance of the work
    member x.Start()    =                                                                                                               
        // Capture the synchronization context to allow us to  raise events back on the GUI thread
        let syncContext = SynchronizationContext.CaptureCurrent()
        // Mark up the jobs with numbers

        let jobs = jobs |> Seq.mapi (fun i job -> (job,i+1))

        let work = 
            Async.Parallel
               [ for (job,jobNumber) in jobs ->
                   async { let! result = job
                           syncContext.RaiseEvent jobCompleted (jobNumber,result)
                           return result } ]
 
        Async.StartWithContinuations
            ( work,
              (fun res-> raiseEventOnGuiThread allCompleted res),
              (fun exn-> raiseEventOnGuiThread error exn),
              (fun exn-> raiseEventOnGuiThread canceled exn ),
             cancellationCapability.Token)
 
    member x.CancelAsync() =
       cancellationCapability.Cancel()
       
    /// Raised when a particular job completes
    member x.JobCompleted  = jobCompleted.Publish
    /// Raised when all jobs complete
    member x.AllCompleted  = allCompleted.Publish
    /// Raised when the composition is cancelled successfully
    member x.Canceled   = canceled.Publish
    /// Raised when the composition exhibits an error
    member x.Error      = error.Publish  
 我们可以用通常的方法来使用这些额外的事件,例如:

    let worker =new AsyncWorker<_>(jobs)  
    worker.JobCompleted.Add(fun (jobNumber, result)->
        printfn "job %d completed with result %A" jobNumber result.Length)

    worker.AllCompleted.Add(fun results->
        printfn "all done, results = %A" results )  
    worker.Start()
这个监督异步工作流能支持取消操作,就像上面例子列出的。
Tweet Tweet, Tweet Tweet(一个WordPress的插件)
用事件触发来报告进度模式可以应用到几乎任何的后台处理组件来进行结果报告。下面的例子,我们将用这个模式来封装从后台读取的来自推特的文章数据流(参考推特 API页面)。这里例子需要一个推特的账号和密码。这个例子中只有一个事件被触发,当然这个例子可以扩展成其他情况下触发其他的事件。
F# JAOO 教程包含了这个例子的一个版本。
// F# Twitter Feed Sample using F# Async Programming and Event processing
#r "System.Web.dll"
#r "System.Windows.Forms.dll"
#r "System.Xml.dll"
 
open System
open System.Globalization
open System.IO
open System.Net
open System.Web
open System.Threading
open Microsoft.FSharp.Control.WebExtensions
 
/// A component which listens to tweets in the background and raises an
/// event each time a tweet is observed
type TwitterStreamSample(userName:string, password:string) =
 
    let tweetEvent = new Event<_>()  
    let streamSampleUrl ="http://stream.twitter.com/1/statuses/sample.xml?delimited=length"
 
    /// The cancellation condition
    letmutable group = new CancellationTokenSource()
 
    /// Start listening to a stream of tweets
    member this.StartListening() = 
        /// The background process
        // Capture the synchronization context to allow us to  raise events back on the GUI thread
        let syncContext = SynchronizationContext.CaptureCurrent()
 
        let listener (syncContext: SynchronizationContext) =
            async { let credentials = NetworkCredential(userName, password)
                    let req = WebRequest.Create(streamSampleUrl, Credentials=credentials)
                    use! resp = req.AsyncGetResponse()
                    use stream = resp.GetResponseStream()
                    use reader = new StreamReader(stream)
                    let atEnd = reader.EndOfStream
                    let rec loop() =
                        async {
                            let atEnd = reader.EndOfStream
                            if not atEnd then
                               let sizeLine = reader.ReadLine()
                                if String.IsNullOrEmpty sizeLine then return! loop() else
                               let size = int sizeLine
                               let buffer = Array.zeroCreate size
                               let _numRead = reader.ReadBlock(buffer,0,size) 
                               let text = new System.String(buffer)
                                syncContext.RaiseEvent tweetEvent text
                               return! loop()
                        }
                    return! loop() }
 
        Async.Start(listener, group.Token)
 
    /// Stop listening to a stream of tweets
    member this.StopListening() =
        group.Cancel();
        group <- new CancellationTokenSource()
 
    /// Raised when the XML for a tweet arrives
    member this.NewTweet = tweetEvent.Publish
每次从推特的标准采样信息流产生一个信息时会触发一个事件,然后提供该帖子的内容。我们可以用以下代码来监听这个数据流:
let userName ="..." // set Twitter user name here
let password ="..." // set Twitter user name here  
let twitterStream =new TwitterStreamSample(userName, password)

twitterStream.NewTweet
   |> Event.add (fun s-> printfn "%A" s)  
twitterStream.StartListening()
twitterStream.StopListening()
当这段代码运行时,将打印出一个的原始XML信息流(非常地快)。参考推特API
来了解这信息流是怎样采样的。
如果你想解析这些信息,这里有一些示例代码实现了类似的功能(当然也请注意推特 API页面上的指导,例如,如果想建立一个高可靠性的系统,这些信息必须在处理前经常的被保存或进行排队)。

#r"System.Xml.dll"
#r"System.Xml.Linq.dll"
open System.Xml
open System.Xml.Linq 
let xn (s:string) = XName.op_Implicit s

/// The results of the parsed tweet
type UserStatus =
    { UserName : string
      ProfileImage : string
      Status : string
      StatusDate : DateTime }

/// Attempt to parse a tweet
let parseTweet (xml: string) =  
    let document = XDocument.Parse xml

    let node = document.Root
    if node.Element(xn"user") <> nullthen
        Some { UserName     = node.Element(xn"user").Element(xn "screen_name").Value;
               ProfileImage = node.Element(xn"user").Element(xn "profile_image_url").Value;
               Status       = node.Element(xn"text").Value       |> HttpUtility.HtmlDecode;
               StatusDate   = node.Element(xn"created_at").Value |> (fun msg->
                                  DateTime.ParseExact(msg,"ddd MMM dd HH:mm:ss +0000 yyyy",
                                                       CultureInfo.CurrentCulture)); }
    else
        None
然后组合子编程能被用来对该数据流进行管道化处理: 
twitterStream.NewTweet
   |> Event.choose parseTweet
   |> Event.add (fun s-> printfn "%A" s)
 
twitterStream.StartListening()

再从该数据流中进行统计: 
let addToMultiMap key x multiMap =
   let prev =match Map.tryFind key multiMap with None -> [] | Some v -> v
   Map.add x.UserName (x::prev) multiMap
 
/// An event which triggers on every 'n' triggers of the input event
let every n (ev:IEvent<_>) =
   let out =new Event<_>()
   let count = ref0
   ev.Add (fun arg-> incr count; if !count % n =0 then out.Trigger arg)
   out.Publish
 
twitterStream.NewTweet
   |> Event.choose parseTweet
   // Build up the table of tweets indexed by user
   |> Event.scan (fun z x-> addToMultiMap x.UserName x z) Map.empty
   // Take every 20’th index
   |> every 20
   // Listen and display the average of #tweets/user
   |> Event.add (fun s->
        let avg = s |> Seq.averageBy (fun (KeyValue(_,d))-> float d.Length)
        printfn "#users = %d, avg tweets = %g" s.Count avg)
 
twitterStream.StartListening()

这里的示例数据流根据用户来进行索引,显示每个用户的平均发言量,每成功解析20个发言就进行一次结果报告

 

#users = 19, avg tweets = 1.05263
#users = 39, avg tweets = 1.02564
#users = 59, avg tweets = 1.01695
#users = 79, avg tweets = 1.01266
#users = 99, avg tweets = 1.0101
#users = 118, avg tweets = 1.01695
#users = 138, avg tweets = 1.01449
#users = 158, avg tweets = 1.01266
#users = 178, avg tweets = 1.01124
#users = 198, avg tweets = 1.0101
#users = 218, avg tweets = 1.00917
#users = 237, avg tweets = 1.01266
#users = 257, avg tweets = 1.01167
#users = 277, avg tweets = 1.01083
#users = 297, avg tweets = 1.0101
#users = 317, avg tweets = 1.00946
#users = 337, avg tweets = 1.0089
#users = 357, avg tweets = 1.0084
#users = 377, avg tweets = 1.00796
#users = 396, avg tweets = 1.0101
#users = 416, avg tweets = 1.00962
#users = 435, avg tweets = 1.01149
#users = 455, avg tweets = 1.01099
#users = 474, avg tweets = 1.01266
#users = 494, avg tweets = 1.01215
#users = 514, avg tweets = 1.01167
#users = 534, avg tweets = 1.01124
#users = 554, avg tweets = 1.01083
#users = 574, avg tweets = 1.01045
#users = 594, avg tweets = 1.0101
 
通过稍微不同的解析,我们可以根据推特提供的采样信息流来显示那些发言超过一次的用户(包括他们的最后一次发言)。这里将在F# Interactive上交互式地执行,并且使用上篇文章的F# Interactive 数据表格片段试图:

open System.Drawing
open System.Windows.Forms 
let form = new Form(Visible = true, Text = "A Simple F# Form", TopMost = true, Size = Size(600,600))

let data = new DataGridView(Dock = DockStyle.Fill, Text = "F# Programming is Fun!",
                            Font = new Font("Lucida Console",12.0f),
                            ForeColor = Color.DarkBlue) 
form.Controls.Add(data)
 
data.DataSource <- [| (10,10,10) |]

data.Columns.[0].Width <- 200
data.Columns.[2].Width <- 500
 
twitterStream.NewTweet
   |> Event.choose parseTweet
   // Build up the table of tweets indexed by user
   |> Event.scan (fun z x -> addToMultiMap x.UserName x z) Map.empty
   // Take every 20’th index
   |> every 20
   // Listen and display those with more than one tweet
   |> Event.add (fun s ->
        let moreThanOneMessage = s |> Seq.filter (fun (KeyValue(_,d)) -> d.Length > 1) 
        data.DataSource <- 
            moreThanOneMessage
            |> Seq.map (fun (KeyValue(user,d)) -> (user, d.Length, d.Head.Status))
            |> Seq.filter (fun (_,n,_) -> n > 1)
            |> Seq.sortBy (fun (_,n,_) -> -n)
            |> Seq.toArray)
 
twitterStream.StartListening() 
这里是一些示例结果:
注意:在上面的例子中,我们使用I/O中断来读取推特信息流。这么做有两个充足的理由:推特信息流是非常活跃的(有时候可能会有信息残留J),我们也可以假设在这许多的推特信息流中有许多未完成的链接,在这种情况只有一个链接,而在任何情况下推特都会限制每个账户监听采样信息流的次数。在下一篇文章中,我们将展示怎么做无中断的读取这种格式的XML信息流。
F# 做并行处理,C#/VB做GUI
用事件触发来报告进度模式在下面这种情况下非常有用,F#程序员实现基于某些输入的后台计算组件,C#或VB程序员使用这些组件。在这种情况下,事件的声明必须用[<CLIEvent>]标记来强调它们作为.NET事件出现(对C#或VB程序员而言)。在上面第二个例子中,你将使用 
    /// Raised when a particular job completes
    [<CLIEvent>]
    member x.JobCompleted  = jobCompleted.Publish 
    /// Raised when all jobs complete
    [<CLIEvent>]
    member x.AllCompleted  = allCompleted.Publish

    /// Raised when the composition is cancelled successfully
    [<CLIEvent>]
    member x.Canceled   = canceled.Publish

    /// Raised when the composition exhibits an error
    [<CLIEvent>]
    member x.Error      = error.Publish 
该模式的限制
用事件触发来报告进度模式是假设在以下情况下的:一个并行处理组件运行在一个GUI应用程序(如Windows Forms)、服务端应用程序(如ASP.NET)或者一些可以触发事件后发回给监听端的程序。这里可以通过调整这个模式来用其他方式触发事件,例如, 发送一个消息到MailBoxProcessor或简单地记录它们。然而要注意的是这个模式还有一个假设:已经有一些主线程或者监听端准备好在任意时刻监听这些事件并且合理地对它们进行排队。
用事件触发来报告进度模式也假设这个封装的对象有能力获取GUI线程的同步内容,正常情况是隐式地(就如上面的例子)。 这通常是一个合理的假设。另外,这些同步内容也可以作为一个显式的参数给出,虽然这不是一个非常普遍的.NET编程习惯。
对那些熟悉IObservable接口(.NET4.0中新增)的程序员,可以考虑用TwitterStreamSample类型来实现这个接口。 然而对于事件的根源,没必要了解那么多。例如, 在将来TwitterStreamSample或许会提供多个事件:当错误发生时报告自动重新连接,或报告中断与延迟。在这情况下,简单地触发.NET事件就足够了,部分地确保你的对象对大部分.NET程序员 来说是熟悉的。在F# 中,所有声明的IEvent<_>值都是自动实现IObservable接口的,所以能直接的用于可观察的组合程序。
结论
用事件触发来报告进度是一种用来封装后台并行处理,同时能够报告结果和进度的强大和优雅的方法。
从表面看,AsyncWorker对象就像单线程一样地效率。 假使你输入的asyncs是隔离的,这意味着组件不会使你程序的其余部分处于多线程的竞争条件下。所有的Javascript框架、ASP.NET框架和GUI框架(如Windows Forms)的用户知道这些框架的单线程形式既是一种祝福也是一种诅咒——你获得了简单(没有数据冲突),但是在.NET编程中并行和异步编程比较困难,I/O和繁重的CPU计算被卸载到了后台线程。 上面的设计模式让你从两边都做到最好:你获得了独立的、能协作的、能“交流的”后台处理组件(包括那些并行处理和I/O),同时又达到了使你大部分代码像单线程GUI编程那样简单。这些组件能被泛型化和重复使用,就像上面展示的那样。这使得它们适合于独立的单元测试。
在以后的文章中,我们将讨论另外的关于F# 异步的并行和交互式编程的一些设计话题,其中包括:
Ø 定义轻量级异步代理
Ø 用异步创建.NET任务
Ø 用异步创建.NET APM模式
Ø 异步取消
 
原文链接:http://blogs.msdn.com/b/dsyme/archive/2010/01/10/async-and-parallel-design-patterns-in-f-reporting-progress-with-events-plus-twitter-sample.aspx
 
你也可以联系F#QQ群:61436709
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值