Hello World
Spiga

F#中的异步及并行模式(1):并行CPU及I/O计算

2010-03-03 23:37 by 老赵, 8299 visits

最后还是忍不住翻译文章了。这系列的文章谈论的是F#中常见的异步及并行模式,作者为F#语言的主要设计者Don Syme。异步相关的编程是F#语言中最重要的优势之一(我甚至在考虑“之一”两个字能否去掉)。F#是一门非常有特色的语言,是一门能够开阔眼界,改变您编程思路的语言,它经过了几年设计以及多个预览之后终于要正式露面了——此刻不上,更待何时。

介绍

F#是一门并行(parallel)响应式(reactive)语言。这个说法意味着一个F#程序可以存在多个进行中的运算(如使用.NET线程进行F#计算),或是多个等待中的回应(如等待事件或消息的回调函数及代理对象)。

F#的异步表达式是简化异步及响应式程序编写的方式之一。在这篇及今后的文章中,我会探讨一些使用F#进行异步编程的基本方式──大致说来,它们都是F#异步编程时使用的模式。这里我假设您已经掌握了async的基本使用方式,如入门指南中的内容。

我们从两个简单的设计模式开始:CPU异步并行(Parallel CPU Asyncs)I/O异步并行(Paralle I/O Asyncs)

  • 本系列的第2部分描述了如何从异步计算或后台计算单元中获得结果。
  • 第3部分(上)则描述了F#中轻量级的,响应式的,各自独立的代理对象。

模式1:CPU异步并行

首先来了解第一个模式:CPU异步并行,这意味着并行地开展一系列的CPU密集型计算。下面的代码计算的是斐波那契数列,它会将这些计算进行并行地调配:

let rec fib x = if x <= 2 then 1 else fib(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异步计算模式的要素:

  1. “async { … }”用于指定一系列的CPU任务。
  2. 这些任务使用Async.Parallel进行fork-join式的组合。

在这里,我们使用Async.RunSynchronously方法来执行组合后的任务,这会启动一个异步任务,并同步地等待其最后结果。您可以使用这个模式来完成各种CPU并行(例如对矩阵乘法进行划分和并行计算)或是批量处理任务。

模式2:I/O异步并行

现在我们已经展示了在F#中进行CPU密集型并行编程的方式。F#异步编程的重点之一,便是可以用相同的方式进行CPU和I/O密集型的计算。这便是我们的第二种模式:I/O异步并行,即同时开展多个I/O操作(也被称为overlapped I/O)。例如下面的代码便并行地请求多个Web页面,并响应每个请求的回复,再返回收集到的结果。

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 -> http site ]
    |> Async.RunSynchronously

上面的代码示例展示了I/O异步并行模式的基础:

  1. “async { … }”用于编写任务,其中包含了一些异步I/O。
  2. 这些任务使用Async.Parallel进行fork-join式的组合。

在这里,我们使用Async.RunSynchronously方法来执行组合后的任务,这会启动一个异步任务,并同步地等待其最后结果。

使用let!(或与它类似的资源释放指令use!)是进行异步操作的基础方法。例如:

let! resp = req.AsyncGetResponse()

上面这行代码会“响应”一个HTTP GET操作所得到的回复,即async { … }在AsyncGetResponse操作完成之后的部分。然而,在等待响应的过程中并不会阻塞任何.NET或操作系统的线程:只有活动的CPU密集型运算会使用下层的.NET或操作系统线程。与此不同,等待中的响应操作(例如回调函数,事件处理程序和代理对象)资源占用非常少,几乎只相当于一个注册好的对象而已。因此,您可以同时拥有数千个甚至数百万个等待中的响应操作。例如,一个典型的GUI应用程序会注册一些事件处理程序,而一个典型Web爬虫会为每个发出的请求注册一个回调函数。

在上面的代码中,我们使用了“use!”而不是“let!”,这表示Web请求相关的资源会在变量超出字面的作用域之后得到释放。

I/O并行的美妙之处在于其伸缩性。在多核的环境下,如果您可以充分利用计算资源,则通常会获得2倍、4倍甚至8倍的性能提高。而在I/O并行编程中,您可以同时进行成百上千的I/O操作(不过实际的并行效果还要取决于您的操作系统和网络连接状况),这意味着10倍、100倍、1000倍甚至更多的性能增强──而这一切在一台单核的机器上也可以实现。例如,这里有一个使用F#异步功能的示例,而最终它们可以在一个IronPython应用程序中使用。

许多现代应用程序都是I/O密集型应用,因此这些设计模式在实践中都有很重要的意义。

始于GUI线程,终于GUI线程

这两个设计模式有个重要的变化,这便是使用Async.StartWithContinuations来代替Async.RunSynchronously方法。在一个并行操作开启之后,您可以指定三个函数,分别在它成功、失败或取消时调用。

对于诸如“我想要获得一个异步操作的结果,但我不能使用RunSynchronously方法”之类的问题,您便应该考虑:

  1. 使用let!(或use!)把这个异步操作作为更大的异步任务的一部分,或者
  2. 使用Async.StartWithContinuations方法执行异步操作

在那些需要在GUI线程上发起异步操作的场景中,Async.StartWithContinuations方法尤其有用。因为,您不会因此阻塞住GUI线程,而且可以在异步操作完成后直接进行GUI的更新。例如,在F# JAOO TutorialBingTranslator示例中便使用了这个做法──您可以在本文结尾浏览它的完整代码,不过这里最值得关注的部分则是在点击“Translate”按钮之后发生的事情:

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 error ocurred: %A" cxn) |> ignore)))

高亮的部分,尤其是在async块里的部分,展示了使用Async.Parallel将一种语言并行地翻译成多种语言的做法。这个异步组合操作由Async.StartWithContinuations发起,它会在遇到第一个I/O操作时立即返回(译注:存疑,为什么是在遇上I/O操作才返回?),并指定了三个函数,分别在异步操作的成功,失败或取消时调用。以下是任务完成后的截图(不过在此不保证翻译的准确性……):

Async.StartWithContinuations有一个重要的特性:如果异步操作由GUI线程发起(例如一个SynchronizationContext.Current不为null的线程),那么操作完成后的回调函数也是在GUI线程中调用的。这使GUI更新操作变的十分安全。F#异步类库允许您组合多个I/O任务,并在GUI线程中直接使用,而无需您亲自从后台线程中更新GUI元素。在以后的文章中我们会进行更详细地解释。

关于Async.Parallel工作方式:

  • 在执行时,由Async.Parallel组合而成的异步操作会通过一个等待计算的队列来逐步发起。与大部分进行异步处理的类库一样,它在内部使用的是QueueUserWorkItem方法。当然,我们也有办法使用分离的队列,在以后的文章中我们会进行一些讨论。
  • Async.Parallel方法并没有什么神奇之处,您也完全可以使用Microsoft.FSharp.Control.Async类库中的其他原语来定义您自己的异步组合方式──例如Async.StartChild方法。我们会在以后的文章中讨论这个话题。

更多示例

F# JAOO Tutorial包含多个使用这些模式的示例代码:

  • BingTranslator.fsx与BingTranslatorShort.fsx:使用F#调用REST API,它们与其他基于Web的HTTP服务的调用方式十分类似。文末包含了示例的完整代码。
  • AsyncImages.fsx:并行磁盘I/O及图像处理。
  • PeriodicTable.fsx:调用一个Web服务,并行地获取原子质量。

本文模式的限制

上文介绍的两个并行模式有一些限制。很明显,使用Async.Parallel生成的异步操作在执行时十分“安静”──比方说,它们无法返回进度或部分的结果。为此,我们需要构建一个更为“丰富”的对象,它会在部分操作完成之后触发一些事件。在以后的文章中我们会来关注这样的设计模式。

此外,Async.Parallel只能处理固定数量的任务。在以后的文章中,我们会遇到很多一边处理一边生成任务的情况。换个方式来看,即Async.Parallel无法处理即时获得的消息──例如,除了取消任务之外,一个代理对象的工作进度是可以得到控制的。

总结

CPU异步并行与I/O异步并行,是F#异步编程中最为简单的两种设计模式,而简单的事物往往也是非常重要而强大的。请注意,两种模式的不同之处,仅仅在于I/O并行使用了包含了I/O请求的async块,以及一些额外的CPU任务,如创建请求对象及后续处理。

在今后的文章里,我们会关注F#中其他一些并行及响应式编程方面的设计方式,包括:

  • 从GUI线程中发起异步操作
  • 定义轻量级异步代理对象
  • 使用async定义后台工作程序
  • 使用async构建.NET任务
  • 使用async调用.NET的APM模式
  • 取消异步操作

BingTranslator代码示例

以下是BingTranslator的示例代码,在运行时您需要申请一个Live API 1.1 AppID。请注意,这个示例需要根据Bing API 2.0进行适当调整,至少在2.0中已经不包含这里的语言检测API了──不过这些代码仍然是不错的示例:

点此展开

原文:Async and Parallel Design Patterns in F#: Parallelizing CPU and I/O Computations

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

32 条回复

  1. 老赵
    admin
    链接

    老赵 2010-03-03 23:37:00

    好吧,我想做F#在国内的推广者。

  2. 麒麟
    *.*.*.*
    链接

    麒麟 2010-03-03 23:43:00

    有没有入门的文章呀 还有就是和C#对比的文章?

  3. 老赵
    admin
    链接

    老赵 2010-03-04 00:00:00

    最后的代码带换行了还真丑……

  4. 老赵
    admin
    链接

    老赵 2010-03-04 00:01:00

    @麒麟
    入门还是很多的吧……找本书看看?

  5. 辰
    *.*.*.*
    链接

    2010-03-04 00:01:00

    好吧,那么我做z#推广者。等微软出了z#再说。

  6. Paladin.lao --VSTO
    *.*.*.*
    链接

    Paladin.lao --VSTO 2010-03-04 00:04:00

    F#,强人呀,我也要学学

  7. 最初的模样
    *.*.*.*
    链接

    最初的模样 2010-03-04 00:08:00

    跟不上时代的步伐了,还是认真学习俺的ruby

  8. Sunny Peng
    *.*.*.*
    链接

    Sunny Peng 2010-03-04 00:09:00

    现在非常忙,很多东西都要学。关注,把你的文章作为读物读,有空能研究下。

  9. 飞林沙
    *.*.*.*
    链接

    飞林沙 2010-03-04 00:39:00

    我还是先把老赵不屑于看的东西给看完吧。。。

  10. 老赵
    admin
    链接

    老赵 2010-03-04 01:14:00

    飞林沙:我还是先把老赵不屑于看的东西给看完吧。。。


    呃?哪写是我不屑于看的啊?

  11. 老赵
    admin
    链接

    老赵 2010-03-04 01:14:00

    Sunny Peng:现在非常忙,很多东西都要学。关注,把你的文章作为读物读,有空能研究下。


    眼界很重要,可以不深入,但是可以知道有这么个东西。:)

  12. 老赵
    admin
    链接

    老赵 2010-03-04 01:18:00

    最初的模样:跟不上时代的步伐了,还是认真学习俺的ruby


    学好了都很好,最近InfoQ上有Matz的采访,并行,Actor模式也是下一代Ruby的目标之一。
    http://www.infoq.com/interviews/yukihiro-matz-language-design

  13. 诺贝尔
    *.*.*.*
    链接

    诺贝尔 2010-03-04 02:21:00

    我相信微软会弄个全自动检测并执行并行算法的牛B框架出来。

    所以,你们先学。

  14. 路过7675[未注册用户]
    *.*.*.*
    链接

    路过7675[未注册用户] 2010-03-04 02:36:00

    对这个东西兴趣还不大...

  15. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2010-03-04 08:57:00

    Jeffrey Zhao:好吧,我想做F#在国内的推广者。


    早就该做了

  16. 老赵
    admin
    链接

    老赵 2010-03-04 09:01:00

    @温景良(Jason)
    其实我一直在做
    只是以前不知道该怎么做
    当然现在还是不知道

  17. 老赵
    admin
    链接

    老赵 2010-03-04 09:02:00

    诺贝尔:
    我相信微软会弄个全自动检测并执行并行算法的牛B框架出来。
    所以,你们先学。


    已然被证明是很困难或是不可能的了,或者说,这需要语言在更基础层面上的支持……
    当然,没有副作用的纯函数式编程语言其实一定程度上已经符合你的要求了。

  18. 麒麟.NET
    *.*.*.*
    链接

    麒麟.NET 2010-03-04 09:20:00

    一直很难理解F#的语法规则

  19. Googol Lee[未注册用户]
    *.*.*.*
    链接

    Googol Lee[未注册用户] 2010-03-04 09:54:00

    只能并行IO的原因是,对cpu的密集操作除非是多核,其实依旧是靠快速调度实现的伪并行,而IO可以借助DMA等外设实现与cpu的操作的并行。展示的GUI并行处理更多是CPU和IO的并行(GUI更新需要CPU,而数据更新需要IO)。

    从实现角度讲,操作系统只有IO操作可以在单线程里异步。而Async.StartWithContinuation应该是在单线程里完成异步,因此会在遇到第一个(可异步的)IO操作时返回。

    !的操作是有要求的么?如果不管是什么赋值都写!有什么问题么?在有了Async.StartWithContinuation的情况下,为什么需要区分普通的赋值和!赋值?

  20. xiao_p
    *.*.*.*
    链接

    xiao_p 2010-03-04 09:56:00

    等的就是这个
    Excellent

  21. 老赵
    admin
    链接

    老赵 2010-03-04 10:01:00

    @Googol Lee
    !和没有不能替代,两者功能完全不同。具体的……看一下Workflow Builder的构建规则就行了,几句话说不清楚。:)
    至于你说的StartWithContinuations,我觉得基本上不是这样。这个方法是通用的发起异步操作的做法,而这个异步操作可能是由Async.Parallel组合而成的,那么,怎么算是“第一个I/O操作”?

    你说的伪并行是站在CPU或OS角度说的,但是对于程序员或是程序来说,100个,1000个任务的并行也是没有问题的。
    有了线程,或是异步消息队列之后,异步操作都没有必要等待什么的,如果真需要的话。
    所以这个只是StartWithContinuations方法的设计选择,没有任何技术方面的限制了,呵呵。
    这东西有机会我还是去确认一下。

  22. Kurodo
    *.*.*.*
    链接

    Kurodo 2010-03-04 10:19:00

    看情况吧,C#就像二奶,漂亮喜欢的人多。F#就像老婆,难看喜欢的人少

  23. 老赵
    admin
    链接

    老赵 2010-03-04 10:29:00

    @Kurodo
    要比喻的话,更像是东西方人审美不同。C#和F#是两个编程范式的语言,用一种范式的习惯去评价另一种范式是不合适滴。

  24. 飞林沙
    *.*.*.*
    链接

    飞林沙 2010-03-04 12:44:00

    @Jeffrey Zhao

    Jeffrey Zhao:

    飞林沙:我还是先把老赵不屑于看的东西给看完吧。。。


    呃?哪写是我不屑于看的啊?



    就是简单的东西

  25. mx1700
    *.*.*.*
    链接

    mx1700 2010-03-04 16:27:00

    这种平行计算是 F# 里特有的吗?
    它和 .NET 4.0 里的Task类有多大区别?
    可以使用C#简单而又方便的实现并行计算吗?
    这篇文章对于我来说太深,看不太明白。
    let! use! 不知道是什么东西。

    我还是先去学学F#基础吧。。。。

  26. 老赵
    admin
    链接

    老赵 2010-03-04 16:37:00

    @mx1700
    应该这么说,F#对于异步/并行/响应式编程的支持很好。
    Task是类库,F#的一些功能基于这套类库。
    简单、方便是相对的,C#比F#不方便,但C#相对于其他一些技术还是方便的。
    let!,use!都是workflow的原语。

  27. 天天不在
    *.*.*.*
    链接

    天天不在 2010-03-04 16:58:00

    在公司刚开始跑不出来.后来加上如下就可以了.

     async { req =  WebRequest.Create(Uri url)
             req.Proxy.Credentials <- new NetworkCredential("name","password","Domain")
    

    听说Google翻译SB成百度.百度一样.
    我也看了下这个输入SB是什么.

    外国人倒是好幽默.

  28. qiaojie
    *.*.*.*
    链接

    qiaojie 2010-03-04 17:57:00

    这个C#中也有Parallel.For啊,一样可以写的简洁,用F#好处不大。

  29. 老赵
    admin
    链接

    老赵 2010-03-04 18:02:00

    @天天不在
    光看这个当然区别不大了,但F#不光只有Async.Parallel方法啊,呵呵。
    Workflow Builder很强大的,用了都说好。

  30. qiaojie
    *.*.*.*
    链接

    qiaojie 2010-03-04 18:40:00

    @Jeffrey Zhao
    恩,若干年前我刚接触Parallel的时候感觉是应该研制一门新的原生支持Parallel的基于FPL的编程语言,F#基本上就是我当初想要的。不过我现在用并行算法的话基本只用基于C++的Parallel.For,因为并行算法毕竟只是一项优化手段,如果不能用并行得到大幅性能提升的话用并行是没意义的,串行程序能解决的问题(指不存在性能问题)用并行去解决只是浪费CPU而已。所以FPL的高阶原语固然好用,但有时未必有用。

  31. 诺贝尔
    *.*.*.*
    链接

    诺贝尔 2010-03-05 10:59:00

    @qiaojie

    最大幅度? 将来除了100核,你就知道痛苦啦。

    框架的好处就是提供一个自适应的环境,不管是1个核,还是100个核,都能利用到7788,这才方便呢。

  32. phoenixcc[未注册用户]
    *.*.*.*
    链接

    phoenixcc[未注册用户] 2010-03-15 16:37:00

    Hi Jeffery,我在尝试文章末尾的代码时发生了一些异常,最终可以简化为:
    //引用部分与文中相同 open xxxx
    let form = new Form (Visible=true, TopMost=true, Height=500, Width=600)
    let textBox = new TextBox (Width=450)

    form.Controls.Add textBox
    在textBox中不管输入什么都会导致程序无响应,fsi.exe被关闭并且在F# InterActive中Session termination detected. Press Enter to restart.

    是否我遗漏了什么东西,其余F#程序正常.本人初学者.

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我