在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)))