F#中的异步和并行设计模式(一):并行CPU和I/O计算

在F#中的异步和并行设计模式:并行化的CPU和I/O计算

F#是一种并行地、交互式的语言。在这点上,我们的意思是:运行F#程序既能有多个实时的赋值(例如:.NET线程主动计算F#的结果)又能有多个等待的响应(例如:等待对事件和消息作出响应的回调和代理)。

编写并行和交互式程序的一个简单方法是使用F#异步表达式。在这篇和接下来的文章中,我将向你介绍F#异步编程的一些基本方法---- 粗略地讲,这些就是通过F#异步编程实现的设计模式。我假设你已经知道了异步的基本知识,如果还不清楚请看这样的入门指南

我们用两个简单的设计模式来开始介绍:并行CPU 异步处理并行I/O 异步处理

第三部分介绍了F#中的轻量级代理、交互式代理、孤立式代理。

模式1: 并行的CPU 异步处理

让我们看看第一个模式的一个例子:并行CPU 异步处理,也就是并行运行一系列受中央处理机限制的计算。下面的代码是计算斐波那契函数与并行计算的时间表。

let rec fib x =if x <= 2then 1elsefib(x-1) + fib(x-2)
let fibs =
    Async.Parallel[ for i in 0..40 -> async { return fib(i) } ]
    |> Async.RunSynchronously

运行的结果是:

val fibs : int array =

[|1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89; 144; 233; 377;610; 987; 1597; 2584;

4181; 6765; 10946; 17711; 28657; 46368; 75025; 121393;196418; 317811;

514229; 832040; 1346269; 2178309; 3524578; 5702887; 9227465;14930352;

24157817; 39088169; 63245986; 102334155|]

上面的示例代码指出了并行CPU 异步处理模式的要素:

a. “async{…}”是用来指定CPU的一些任务数。

b.  这些通过fork-join机制与Async.Parallel融合到并行中去

在这种情况下,使用Async.RunSynchronously来执行该组合,即启动一个异步实例并同步等待整体的结果。

您可以在很多日常的CPU并行工作(如分割和并行矩阵乘法)和批处理作业中使用这种模式。

模式2:并行I/O 异步处理

到目前为止,我们只看到与F#有关的受中央处理机限制的并行编程。关于F#异步编程很重要的一点是您可以用它来进行CPU和I/O计算。这带来了我们的第二种模式:并行I/O 异步处理,如并行进行I/O操作(又称为重叠I/O)。例如,下面的代码并行地请求多个网页以及为每一个请求提供响应,并返回所收集的结果。

open System
open System.Net
open Microsoft.FSharp.Control.WebExtensions

let http url =
    async { let req = WebRequest.Create(Uri url)
            use! resp = req.AsyncGetResponse()
            use stream = resp.GetResponseStream()
            use reader = new StreamReader(stream)
            let contents = reader.ReadToEnd()
            return contents }

let sites = ["http://www.bing.com";
             "http://www.google.com";
             "http://www.yahoo.com";
             "http://www.search.com"]

let htmlOfSites =
   Async.Parallel [for site in sites -> httpsite ]
    |>Async.RunSynchronously

上面的代码指出了并行I/O 异步处理模式的本质:

(a)”async{…}”是用来编写包括一些异步I/O的任务。

(b)这些通过fork-join组合Async.Parallel融合到并行中去

在这种情况下,使用Async.RunSynchronously执行的组件,同步等待整体的结果。

使用let!(或与其等价的资源处理命令use!)是组成asyncs的一个基本途径。像这样的一行代码:

let! resp = req.AsyncGetResponse()

当HTTP GET的响应发生时,会产生一个“响应”。也就是说当AsyncGetResponse操作完成时,剩下的async{…}代码块会运行。然后,在等待这个响应的过程中,既没有.NET也没有操作系统的线程被阻塞,因为只有活动的CPU计算才使用一个底层的.NET或者O/S线程。相比之下,响应等待的代价(如回调,事件处理程序和代理)是相对便宜的,通常情况下就像单个的注册对象那么便宜。因此你可以拥有数千甚至上百万的响应等待。例如,一个典型的GUI应用程序有许多注册的事件处理函数,一个典型的网络爬虫为每一个重要的web请求提供一个注册的处理函数。

在上面,“use!”代替”let!”表示跟web请求相关的资源应该在变量作用域结束后被释放掉。

有关I/O并行化的好处之一是缩放。随着多核的受CPU限制的编程,如果你在一台多核的机器上付出足够多的努力,经常会有2倍、4倍或8倍的加速。你可以用I/O的并行编程进行成百上千的并行操作(虽然实际并行取决于你的操作系统和网络连接)来10倍、100倍、1000倍或更多倍地提高速度,甚至在一个单核的机器上。例如,这个使用F# 异步处理的漂亮的例子,最后被一个Iron Python应用程序调用。

许多现代应用程序是受I/O限制的,所以重要的是能够在实践中识别和运用这种设计模式。

在GUI线程上启动,在GUI线程上结束。

这两个设计模式有一个重要的变化,这就是用Async.StartWithContinuations代替Async.RunSynchronously。这里,你指定三个函数分别在异步线程成功结束、失败结束或取消结束时作为并行线程组运行。

每当你面临“我需要得到一个异步的结果,但我真不想使用RunSynchronously”这样问题的时候,那么你应该考虑:

a.      通过let!或use!把这个异步代码块作为一个更大的异步代码的一部分来开始,或者

b.     用Async.StartWithContinuations 来开始这个异步程序

Async.StartWithContinuations在GUI线程上进行异步执行时是非常有用的,因为你永远也不会希望去阻止这个GUI线程,相反,你想在异步完成时安排发生一些GUI的更新。例如,在F# JAOO 教程代码中的BingTranslator示例就是这样用的。在这个博客帖子的底部,给出了此示例的完整版,这里最重要的是要注意到当“翻译”按钮被按下时会发生什么:

button.Click.Add(fun args ->
    let text = textBox.Text
    translated.Text <-"Translating..."
    let task =
        async { let! languages = httpLines languageUri
                let! fromLang = detectLanguage text
                let! results = Async.Parallel [for lang in languages ->translateText (text, fromLang, lang)]
                return (fromLang,results) }
    Async.StartWithContinuations( 
        task,
        (fun (fromLang,results) ->
            for (toLang, translatedText) in results do
                translated.Text <-translated.Text + sprintf "\r\n%s --> %s:\"%s\"" fromLang toLang translatedText),

        (fun exn -> MessageBox.Show(sprintf"An error occurred: %A" exn) |> ignore),
        (fun cxn -> MessageBox.Show(sprintf"A cancellation errorocurred: %A" cxn) |> ignore)))

在高亮部分,指定了这个异步处理的内容,它包括使用Async.Parallel把输入的文本并行地翻译成多国语言。这里的复合异步是用Async.StartWithContinuations启动的,这会使异步线程在它首次运行I/O操作时尽可能快地被疏导,同时指定三个函数分别在异步线程成功结束、失败结束或取消结束时运行。这里是任务结束时的一张截图(关于翻译的准确性不能保证)

  图片===

Async.StartWithContinuations 有一个很重要的属性 ——如果异步开始于GUI线程(例如:一个非空的SynchronizationContext.Current线程),那么结束函数会在这个GUI线程上被调用。这确保了结果更新的安全。F# 异步类库允许你指定要合成的I/O任务,然后在GUI的线程上运行,这样就不必在后台的线程中配置你的更新了,我们将在接下来的帖子中解释这个话题。

关于Async.Parallel的工作机制需要注意的地方:

·        在运行时,组成异步处理的Async.Parallel最初是通过一个请求计算队列开始的。和大部分异步处理类库一样,最终这些操作会使用QueueUserWorkItem。使用多个队列也是可以的,我们会在接下来的帖子中讨论到。

·        Async.Parallel并没有什么特别神秘的:你可以用位于Microsoft.FSharp.Control.Async类库中的其他基本函数(如Async.StartChild)来自定义以不同异步方式协作的异步处理机制。我们会在以后的帖子中再来讨论这个话题。

更多示例:

F#JAOO教程中使用了这些模式的例子:

·        BingTranslator.fsx 和BingTranslatorShort.fsx:用F# 调用一个REST API.这与任何类似的基于web的HTTP服务相似。这个示例的版本在下面已经给出。

·        AsyncImages.fsx:并行磁盘I/O和图形处理

·        PeriodicTable.fsx:调用一个web service,获取并行的原子量(元素表)

并行计算的局限性:

这里介绍的两大并行模式有一些局限性。值得注意的是,一个由Async.Parallel产生的异步线程在运行的时候是无法交互的——例如,它不报告进度或部分结果。为了打破这样的局限,我们需要另外建立一个可交互的对象来触发部分操作完成事件。我们会在接下来的文章中关注这个的设计模式。

另外,Async.Parallel只处理固定数量的作业。在后面的文章中,我们会看到很多这样的例子——作业被当作工作进程而生成。换一种方式来看待这个局限性,就是:Async.Parallel产生的异步不立即接受传入的信息。既:它不是一个进度可以被驾驭的代理,除了取消。

由Async.Parallel生成的异步操作确实支持取消。除非所有的子任务已经完成或者已被有效取消掉,否则取消是无效的。这通常是你想要的。

结论:

并行CPU异步操作和并行I/O异步操作模式可能是F# 异步编程中最简单的两种设计模式。通常情况下,简单的东西是重要的和功能强大的。请注意,这两个模式的唯一不同点在于并行I/O模式使用的异步处理包含了(而且往往占主导地位)I/O请求,加上一些CPU 处理创建请求对象并做后期处理。

在将来的博客中,我们将关注其他有关并行和交互的F#异步编程的设计话题, 包括:

·        从GUI线程启动异步操作

·        定义轻量级的异步代理

·        定义使用异步操作的后台工作组件

·        使用异步创建.NET任务

·        使用异步操作创建.NET APM(Asynchronous Programming Model异步编程模块)模式

·        取消异步操作

 

BingTranslator 代码示例

下面是BingTranslator示例的代码。 你需要一个Live API 1.1 AppID来运行它。

(注意:这个例子可能需要为Bing API 2.0做适度的调整,特别是语言检测的API在2.0不存在,然而,这些代码应该还是可以作为一个好的指导)

open System
open System.Net
open System.IO
open System.Drawing
open System.Windows.Forms
open System.Text
  
/// A standard helper toread all the lines of a HTTP request. The actual read of the lines is
/// synchronous once theHTTP response has been received.
let httpLines (uri:string) =
  async { let request = WebRequest.Createuri
          use! response = request.AsyncGetResponse()         
          use stream = response.GetResponseStream() 
          use reader = new StreamReader(stream) 
          let lines = [ while not reader.EndOfStream do yield reader.ReadLine() ]
          return lines }

type System.Net.WebRequest with  
    /// An extension member to write content into anWebRequest.
    /// The write of the content is synchronous.
    member req.WriteContent (content:string) = 
        let bytes =Encoding.UTF8.GetBytes content
        req.ContentLength <- int64bytes.Length
        use stream =req.GetRequestStream()
        stream.Write(bytes,0,bytes.Length)  
  
    /// An extension member to read the content from aresponse to a WebRequest.
    /// The read of the content is synchronous once theresponse has been received.
    member req.AsyncReadResponse () =
        async { use! response = req.AsyncGetResponse() 
                use responseStream = response.GetResponseStream() 
                use reader = new StreamReader(responseStream)
                return reader.ReadToEnd() }

#load @"C:\fsharp\staging\docs\presentations\2009-10-04-jaoo-tutorial\BingAppId.fs"
//let myAppId ="please set your Bing AppId here"   
/// The URIs for the RESTservice we are using
let detectUri       = "http://api.microsofttranslator.com/V1/Http.svc/Detect?appId=" + myAppId
let translateUri    = "http://api.microsofttranslator.com/V1/Http.svc/Translate?appId=" + myAppId +"&"
let languageUri     = "http://api.microsofttranslator.com/V1/Http.svc/GetLanguages?appId=" + myAppId
let languageNameUri = "http://api.microsofttranslator.com/V1/Http.svc/GetLanguageNames?appId=" + myAppId

/// Create the userinterface elements
let form       = new Form (Visible=true, TopMost=true, Height=500, Width=600)
let textBox    = newTextBox (Width = 450, Text ="Enter some text",Font =newFont("Consolas", 14.0F))
let button     = newButton (Text="Translate", Left = 460)
let translated = newTextBox (Width = 590,Height = 400, Top = 50, ScrollBars = ScrollBars.Both, Multiline = true, Font=new Font("Consolas", 14.0F))
form.Controls.Add textBox
form.Controls.Add button
form.Controls.Addtranslated

/// An async method tocall the language detection API
let detectLanguage text =
  async { let request = WebRequest.Create(detectUri, Method="Post", ContentType="text/plain")
          do request.WriteContent text
          return! request.AsyncReadResponse() } 

/// An async method tocall the text translation API
let translateText (text,fromLang, toLang) =
  async { let uri = sprintf "%sfrom=%s&to=%s" translateUri fromLangtoLang
          let request = WebRequest.Create(uri, Method="Post", ContentType="text/plain")
          request.WriteContent text
          let! translatedText =request.AsyncReadResponse()
          return (toLang, translatedText)}  

button.Click.Add(fun args ->   
    let text = textBox.Text
    translated.Text <- "Translating..."   
    let task =
        async { /// Get the supportedlanguages
                let! languages = httpLineslanguageUri
                /// Detect the language ofthe input text. This could be done in parallel with the previous step.
                let! fromLang = detectLanguagetext
                /// Translate into eachlanguage, in parallel 
                let! results = Async.Parallel [for lang in languages -> translateText (text,fromLang, lang)]
                /// Return the results
                return (fromLang,results)} 

    /// Start the task. When it completes, show theresults.
    Async.StartWithContinuations( 
        task,
        (fun (fromLang,results) ->
            for (toLang, translatedText) in results do
                translated.Text <- translated.Text +sprintf "\r\n%s --> %s: \"%s\"" fromLang toLang translatedText),
        (fun exn -> MessageBox.Show(sprintf "An error occurred:%A" exn) |> ignore),
        (fun cxn -> MessageBox.Show(sprintf "A cancellationerror ocurred: %A" cxn) |> ignore)))
 

 原文链接:http://blogs.msdn.com/b/dsyme/archive/2010/01/09/async-and-parallel-design-patterns-in-f-parallelizing-cpu-and-i-o-computations.aspx

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值