从简单的 F# 表达式构建并发应用程序
本文基于 F# 的 Visual Studio 加载项和相关技术的预发布版撰写而成。文中的所有信息均有可能发生变更。
本文将介绍以下内容:
|
本文使用了以下技术: F# |
许多程序员都认为异步编程很难,因此他们创建了很多能够像异步应用程序一样高效运行的同步版本程序,而这仅仅是为了避免假定存在的异步代码的复杂性。
一直以来,这些做法都没导致什么后果。但是,自从新的分布式计算环境(如多核处理器和面向服务的体系结构)出现后,开发人员开始感到了压力。有关此问题的一个典型示例出现在 I/O 操作中,其中,处理器时间在 I/O 绑定的操作执行的同时可能会得到更有效的利用。对许多应用程序而言,其所处位置要么需要读取和处理大量文件,要么需要调用 Web 服务的复杂业务流程以检索数据并对这些数据进行处理然后发送给其他服务。
实际上,异步程序是一种比同步程序更加简单的排列。异步程序故意不明确指出要执行的命令的顺序。这是一种比较懒惰的规范,但也因此更容易定义。
开发人员在进行异步编程时经常会走的一个弯路就是他们过于关注并发操作问题,以至于忽略了程序核心应具有的简单性。Lock、semaphore 以及其他一些并发计算工具更倾向于使程序布局较少关注其核心逻辑而更多关注其资源的并发管理。
而这恰恰是 F# 中异步表达式的价值所在。您可以从并发控制流中分离出简单的程序并揭示核心程序的简单性和可读性。另外,F# 语言的其中一个最强大方面就表现在它可以创建能从其他任何符合 Microsoft .NET Framework 的语言进行无缝调用的库,这将允许您通过现有的命令式 C# 或 Visual Basic 代码充分利用 F# 异步表达式所提供的直观并行性。
在本文中,我将说明 F# 中异步表达式的本质。我会提供一些实现示例,还会为您展示如何将您自己的框架并入简单的异步基元中。使用我在这里讨论的技术,您将能够创建模块化的、富于表达性的程序来解决大多数极具挑战性的并发问题。
传统记法的问题
下面是一个有关调用方法或函数的传统示例:
commandOne(); commandTwo(); commandThree();
毫无疑问,在这一传统记法中,commandTwo 跟在 commandOne 之后,而且只有当其完成后才会执行 commandThree。
在这种记法中,如果想指定您并不关心这些命令的执行顺序会比较困难。它应该像在这些命令之间加一个分隔符那样简单:
commandOne() | commandTwo() | commandThree()
对于不关心其执行顺序的这三个命令而言,类似的这种方法可能会是很有用的记法。但或许更为有用的是一个可指示您希望这些命令如何运行的构造(而不是使用分隔符),如下所示:
async {commandOne()}; async {commandTwo()}; async {commandThree()}
假设您想将某个值设置为第一个完成的命令的结果:
x = first( async { commandOne() }; async { commandTwo() }; async { commandThree()}; )
这看似简单的指令将成为一些最流行模式中的主要工作任务。
从这个角度看,分号不太像是语句的终止,更像是一个运算符,用于在当前语句完成之前阻止后续语句的执行。对“同步程序是程序的一个更为具体的定义”这一说法可以从这一角度直观地看出来。
在编程语言设计方面所做的诸多改进为解决异步程序设计所面临的问题燃起了一盏新的希望之灯。将其归结为编程语言问题后,您马上就会找到多种更具表达性、更清晰而且更简洁的构建记法的途径。使用异步表达式,F# 团队不但朝着该目标更近了一步,而且还提升了编程人员与 .NET 社区尚无权访问的编程语言之间的交互级别。
函数式编程
您可能已经在 Ted Neward 的入门读物 (msdn.microsoft.com/magazine/cc164244) 中看到了许多有关 F# 的内容。考虑到那些对此语言还很陌生的读者,我将回顾几个要点,但仅限对介绍 F# 中的异步表达式所必需的信息。
本文中的所有代码示例使用的均为 #light 编译器指令(尽管可能不是显式使用的)。该指令会使编译器根据每行代码与其他代码的对齐情况对代码行进行解析。虽然这会得到条理清晰的布局,但同时也会使代码主体包含大量空白区域。当您自行研究本文中的代码时,一定要遵循示例中使用的对齐方式,应使用空格而不是制表符。要使用 #light 指令,请将其置于它要施加影响的代码上方:
#light System.Console.WriteLine "Hello World"
本文附带的所有代码均可进行编译,但为了便于执行可考虑使用 F# 交互式控制台。要启动此控制台,请打开 Visual Studio。单击“视图”|“其他窗口”|“F# 交互式”。
F# 的一个重要特点是所有内容都是一个值,甚至函数也如此。默认情况下,所有内容都是一个不变的值。以下列代码行为例:
let x = 2
这将产生一个值 2(可将其视为 x)。值 x 在这之后不能更改为其他值。在其他作用域内,或许 x 完全可以指向其他值,但在此作用域内却不能更改。值还可以作为表达式的结果加以创建。在下行中将把结果 5 绑定到 x:
let x = 2 + 3
在这种设置中,您基本上可以开始不把等号视为赋值符号,而是看作一个用来指示右侧等于左侧的声明。这种值设置情况也适用于函数。如果需要频繁写入控制台,我可以为字符串创建一个缩写值 emit,用来表示 Console 类的 WriteLine 方法:
let emit:string->unit = System.Console.WriteLine
值 emit 现在将代表控制台方法。冒号之后的内容是函数的类型签名。在这里此签名表示函数将接受字符串并返回 unit(unit 类似于 C# 中的 void)。同样,值 emit 不可变,无法表示此作用域中的任何其他项目。因此您会发现,在 F# 中方法和函数均为值。
现在我已将一个值设置成了函数,知道如何调用它们会非常令人高兴。毕竟,这就是我们通常运行程序的方式,不是吗?
在函数式编程中,我们将使用术语“应用”(application) 而不是“调用”(invocation)。这只是函数对参数的应用。在 F# 中,函数必须在已应用到它的所有参数后才会被调用。
在函数式设置中,将函数应用到它的参数的过程看起来略有不同。由于在函数式编程中需要频繁地将函数应用到各个值,因此使用侵入性最低的以下运算符来指示应用:空格。例如,要应用 emit(表示函数 Console.WriteLine),可如下所示编写代码:
emit "Hello World"
此代码将产生以下输出结果:
Hello World
或者,也可以编写如下所示的代码:
emit("Hello World")
有时,C# 开发人员会对这种灵活性会感到困惑,因为看上去其方法调用十分相似。括号仅用于分组参数,这就是为何编译器允许排除空格的原因。在本文中采用的规则是,只有在需要将参数和术语组合在一起时才使用括号。
最后,有些函数采用参数 void。在 F# 中,这被称为 unit,由空的括号表示:
let emitter () = emit "Hello World" emitter ()
此代码将产生如下所示的输出结果:
"Hello World"
异步表达式
现在来看一下异步表达式。图 1 显示了一个同步程序,它读取目录中的文本文件并计算每个文件中的空格字符数。函数 countSpace 将获取文件名称和大小,并打开一个流来读取相应的字节数。然后,它将在列表折叠运算中计算空格所占的字节数。在它下面,值 "files" 是该目录中的文本文件的数组,而值 "counted" 将使用 Array.map 来计算每个文件的函数 countSpace。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
#light open System.IO open System let countSpace filename size = let space = Convert.ToByte ' ' use stream = File.OpenRead filename let bytes = Array.create size space let nbytes = stream.Read (bytes,0,size) let count = bytes |> Array.fold_left (fun acc x -> if (x=space) then acc + 1 else acc) 0 count let files = (DirectoryInfo @"C:\Users\Chance\Documents").GetFiles "*.txt" let counted = files |> Array.map (fun f -> countSpace (f.FullName) (int f.Length))
有人可能会希望在此程序的结尾有一个整数数组来指示每个文件中的空格数。那么创建一个并行版本的工作量有多大?
使用异步表达式创建并行版本是指通过函数来创建表达式,具体方法是将函数放在大括号中并前置一个标识符 async(参见图 2)。此表达式现在的类型是 Async<int>,这是 aCountSpace 在应用到它的两个参数时返回的类型。与同步版本的唯一实质性差别在于流将调用 ReadAsync 而不仅仅是读取。此方法可通过 Microsoft.FSharp.Control.CommonExtensions 命名空间获得。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
open Microsoft.FSharp.Control.CommonExtensions #nowarn "057" let aCountSpace filename size = async { let space = Convert.ToByte ' ' use stream = File.OpenRead (filename) let bytes = Array.create size space let! nbytes = stream.ReadAsync (bytes,0,size) let count = bytes |> Array.fold_left (fun acc x -> if (x=space) then acc + 1 else acc) 0 return count }
现在必须将函数应用到参数以获取表达式,并且必须执行该表达式。这就是获取最终计数数组的真正差别所在。与计算计数数组时一样,Array.map 函数也会将 aCountSpace 应用到每个文件。但是,应用该函数只会返回一个可根据需要执行的异步计算(因为 aCountSpace 返回 Async<int>)。生成的异步计算数组随后被发送给 Async.Parallel 基元,它会将异步计算数组转换为一个可以返回结果数组的异步计算。然后,表达式将被传递给 Async.Run 函数:
let aCounted = files |> Array.map (fun f -> aCountSpace (f.FullName) (int f.Length)) |> Async.Parallel |> Async.Run
Async.Run 执行最终的操作,运行异步计算并返回结果。
使用 let!和 return!
您可能已经注意到,在 stream.ReadAsync 调用的另一侧有一个感叹号。这并不是在无意间添加的。典型的 let 绑定会为值创建一个名称。异步表达式具有一种新技术,可绑定到其他异步表达式的结果。此方法被用来创建可由其他表达式构成的异步计算。正如我之后将要说明的,这正是异步表达式的关键值。
您可以根据由表达式得出的值来了解该表达式。此代码可被看作是整数 1 并进行相应处理:
async { return 1}
但是,如果您想要处理异步表达式所生成的值,可能会遇到一点麻烦,如以下示例所示:
let x = async {return 1} let y = async { let res = x return 5 + res } // TYPE ERROR
执行表达式的组合时需要进行特殊的考虑。您需要略微改变一下运算符,以指示您想要在当前表达式执行的同时执行此异步表达式。您不想创建表达式 x;而是想异步运行该表达式并将 x 与其生成结果绑定在一起。res 的类型应为 int,而非上例所示的 Async<int>。
用于洞悉表达式并访问底层类型的运算符是 let!。当然,在 Async.Run 应用到整个组合之前该表达式实际上并未执行。
let x = async { return 1} let y = async { let! res = x return 5 + res } // WORKS! Async.Run y
这得到的结果自然是整数 6。
与 let! 类似的还有 return!,它用于将另一个表达式直接组合到返回语句中:
let x = async { return 1} let y = async { return! x}
虽然不是一个十分恰当的例子,但它也说明了这一点。当您处理嵌套表达式时,let!和 return!可使您避免在考虑如何将表达式绑定到一起时劳神费力。相反,您只需将这些表达式视为它们返回的值即可。实际上,上述示例也可以仅使用 let! 来编写,如下所示:
let x = async {return 1} let y = async { let! temp = x return temp }
这种对组合性的关注是此记法所体现的附加值的关键所在。它使程序员可以将注意力放在基本程序逻辑上,而将其与对计算的并行特性的说明区分开来。
可使用 Async.Run 进行阻隔,直到异步计算完成为止。但前面所示代码中的 Async.Parallel 示例仍未完全解释清楚。此函数可以应用到异步表达式数组,对于数组它将返回一个异步表达式。Async.Parallel 是一个异步基元,它所包含的一系列函数旨在将新的执行逻辑组合到异步表达式中,同时充分利用内置的异常处理功能和组合性。
基元的理念是控制表达式在最终应用时的执行方式。例如,在插入执行流逻辑以与其他进程并行运行之前,异步表达式(例如此处所示的)并不会实际异步运行:
let one = async { return 1 }
优秀基元的关键在于它们是以可重用的模式来异步管理计算的。您可将 Async.Parallel 应用到异步表达式数组(Async<'a> 数组),它将返回一个异步表达式,而该表达式返回的是生成类型的数组(Async<array<'a>>)。在执行时,数组中的每个表达式均并行执行。Async.Parallel 会以单独的线程执行各个成员,并会处理在返回数组的对应元素中管理每个线程结果的所有细节。
对异步表达式而言,Async.Parallel 是有关可重用执行方法的一个很好示例。当数组中的元素数与计算机的处理器内核数相同时,它尤为有用。(您可不必担心具体匹配问题 — 线程池将创建和管理正确的线程数并相应地规划异步工作。)要快速生成数组,可使用 F# 中的 [| … |] 生成记法:
let nums = [|async {return 1 + 1}; async {return 1+ 2}; async {return 1 + 3}|] Async.Run (Async.Parallel nums)
您先前看到的另一个基元是流中的 ReadAsync(Stream.Read 的异步版本),它可读取文件数据。该基元(当 Microsoft.FSharp.Control.CommonExtensions 命名空间打开时)存在于读取数据的流中。此函数可应用到缓冲区数组、起点和长度。它返回 Async<int>,指示所读取的字节数。当然,所有这些只有在执行函数(如 Async.Run 或 Async.Spawn)被应用到整个表达式时才会执行。
这些基元是构成组合的构建块。当它们被相应构建后,您可以模块化并行计算的控制流详细信息并将其从实际的程序逻辑中分离出来。对该程序的表示而言,其抽象程度足以使您能够开始使用新函数作为计算设备来以不同的方式执行这些程序。
这两种基元均为异步表达式,类似于之前创建的基元。但是,当对其应用 Async.Run(或另一个执行函数,如 Async.Spawn)时,它们将会开始执行,以处理其异步方法的详细信息。这些计算类型作为异步表达式对齐后会产生富有表现力的组合体系结构,利用它可以选择如何运行您的程序。构建大型的复杂系统的方法是:将程序与简单的基元集合在一起。
创建您自己的基元
对于将不同表达式集成在一起的基元,技术人员已经做了周密的考虑,它完全可以处理您需要创建的绝大多数逻辑。虽然功能强大的库是简化许多任务的可靠途径,但开发可高度自定义的解决方案以满足特定情况下的某些具体需求可能会带来更大的价值。异步表达式提供了随着需求的增加来添加自定义基元的途径。在本章节中,我将向您介绍一个示例来说明新异步基元的实现。
对于大多数命令式语言都相当困难的一件事情是如何在另一个调用完成后能够尽快处理 I/O 请求。请考虑 C# if 语句:
if ( a || b) {Console.WriteLine("A and B were true");} else { Console.WriteLine("Either A or B was false");}
如果 a 为 true,则不检查 b,而执行第一个语句。如果调用的是冗余数据源,那么 I/O 操作中的短路(其目标是获取第一个成功结果)可能非常有用。在这种情况下,只需检查第一个返回值的数据源即可。如果该数据源是数据库、平面文件、Web 服务或外部传感器,则这一切都无关紧要。
对于命令式语言,这并不是一件简单的事。任何尝试都有导致出现并发问题的风险,因为有可能无法将并发控制从检索第一个可用数据的简单表达式中分离出来。异常处理等问题会进一步加剧复杂性。
例如,我们将数据请求发送给 Microsoft TerraService 数据库。我首先在 WebReferences 命名空间中引用 TerraService.dll 文件:
#light #r "TerraService.dll" #nowarn "57" open System open System.Xml open System.Xml.Serialization open System.IO open Microsoft.FSharp.Control open Microsoft.FSharp.Control.CommonExtensions open WebReferences
前面曾提到过,所有示例都使用 #light 编译器指令。由于异步表达式到目前为止还是一个试验性的功能,因此我加入 #nowarn "57" 以忽略警告。我还需要打开多个命名空间(包括 WebReferences 命名空间),以提供对 TerraService Web 服务代理的访问。
下一步是使用可辨识联合来设置几个类型:
type 'a Result = | Complete of 'a | Failure of 'a type 'a RetrievedValue = | File of 'a | Web of 'a | Timeout | Error let ts = new TerraService()
Result 类型提供了有关终止计算过程的说明,RetrievedValue 描述了 TerraService 调用的源以及可能的错误。我要把结果缓存到文件系统,因此 File 和 Web 都会作为模式包括在联合中。最后,实例化新的 TerraService 类型。
基元和支持函数
对于大多数 F# 代码而言,最好从内向外、从下往上读取。在图 3 中,要了解的第一件事是函数 first,它是真正的异步基元。first 的唯一任务是创建异步基元,对于在上述可辨识联合中返回其中一个 Result 类型的那些异步表达式,此异步基元将针对其列表加以执行。
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
let updateAndContinue n i cont res = lock n (fun () -> if !n = -1 then n:=i) if (!n=i) then cont res let aContinueFirstSuccess cont n i x = async { let! y=x in do match y with | Complete(res) -> updateAndContinue n i cont res | _ -> () } let continueIfFirst cont n i x = Async.Spawn (aContinueFirstSuccess cont n i x) let first (ls:Async<'a Result> List) = Async.Primitive (fun (cont,exn) -> let n = ref (-1) in List.iteri (continueIfFirst cont n ) ls)
与所有异步基元一样,其核心部分也是一个函数,它执行延续函数 (cont) 和异常函数 (exn)。具体调用哪一个取决于表达式计算的实际情况,但最终还是由开发人员来定义在何种情况下执行各个分支。
此函数首先会创建一个对整数的引用(初始值为 -1)。F# 中的引用单元格是创建可变值的一种方式。与 C# 中的变量十分相似,它们也可通过 := 操作符进行赋值。它们的值可通过在名称前添加一个感叹号来进行访问(如图 3 中的 updateAndContinue 函数所示)。
负一是一个标记,表示引用尚未被设置。接下来,会使用 List.iteri 构造来迭代异步表达式的列表,并针对每个表达式使用 cont 和整数引用 n 调用 continueIfFirst。与 List.iter 不同,List.iteri 将应用待迭代列表中的元素的整数索引,以用于标识每个执行的表达式。
函数 continueIfFirst(正好在函数 first 上方)会生成新的线程并使用延续函数 cont、整数引用、异步表达式列表中的索引(从 first 中的 ls 开始)以及要执行的表达式来触发异步表达式 aContinueFirstSuccess。
异步表达式 aContinueFirstSuccess(现在正在单独的线程中对异步表达式的原始列表中的每个元素进行处理)会在当前线程中执行异步表达式并检查其结果。回想一下,在基元的实现过程中,每个异步表达式都会从绑定到某个泛型类型(由 'a 表示)的可辨识联合返回其中一个 Result 类型。如果结果是 Complete,则代码将通过调用 updateAndContinue 并向其传递相同的参数来进行更新并继续执行(原始异步表达式除外)。函数 updateAndContinue 可确保只有第一个更新 n 的表达式能够继续。引用单元格 n 将使用锁以线程安全的方式执行更新,然后会检查该值是否等于 i。如果引用单元格 n 和 i 相等,则此线程必须是第一个更新 n 的线程。此时表达式已计算完毕,因此我将结果作为 res 传递下去。
最终,基元 first 及其为数不多的支持函数将提供一种执行机制,生成异步计算的列表并返回第一个成功结果。
此代码中最引人注目的内容之一就是超时机制。毕竟,虽然最终总会返回一组计算中的第一个结果,但对于用户而言,他们能够等待的时间都有一个最大限制值。
下面是创建超时所必需的两个函数。第一个函数是 timeout 函数,它用来获取超过超时时间(毫秒)时返回的值:
let timeout i x = async { do System.Threading.Thread.Sleep(i:int) return Complete(x) }
第二个函数是 terminateInSeconds。请注意,此函数需要在某个现有的异步表达式之上构建:
let terminateInSeconds time zero expr = let success = async { let! res = expr return Complete(res) } first [ success; timeout (1000*time) zero]
使用 first 时,它将执行所分配的表达式,如果在给定时间内此表达式未能成功执行则返回零参数。
执行异步计算列表可实现的一个非常简单的用途就是创建倒计时。倒计时可以是一个异步表达式,定义方式如下:
let rec countdown x = async { do Console.WriteLine(x:int) do System.Threading.Thread.Sleep(1000) if x = 0 then return Complete("Done") else let! res = first [ countdown (x-1); timeout ((x-1) * 1000) "Launch"] return Complete(res) }
如果使用 Async.Run 基元运行此表达式,并向其传递参数 5,则会得到间隔为 1 秒的倒计时:
> Async.Run (countdown 5);; 5 4 3 2 1 0 val it : string Result = Complete "Launch"
请注意,结果将是 "Launch",而非 "Done"。虽然每种情况下都会引发超时,但只有当触及到递归链中的下一个倒计时的时候,它才会终止异步计算。如果它使用 Async.Spawn 运行,也会发生同样的情况,但它会立即打印输出表达式的返回值,并且您可以在倒计时执行过程中执行其他操作。
将异步调用扩展到 Web
我真的很希望将这一快捷方式加入到我的 TerraService Web 服务调用中。所有 I/O(无论来自文件系统、数据库、Web 甚至是复杂遥测)都可以包括在此类业务流程中。当您在连接很慢或不太可靠的情况下创建冗余数据源时,您可能就会发现它很有用。将全球软件部署到连接基础结构较差的环境中时,通常就会面临这种情况。
我编写了两个异步表达式,这两者本身都不会执行任何异步操作。第一个表达式是 aGetPlaceWeb,它采用三个字符串,分别指示某个位置所属的市、国家/地区和州/省。然后它使用 TerraService 检索该位置并使用 savePlace 将其保存到本地文件中:
type TerraService with member ts.AsyncGetPlaceFacts(p) = Async.BuildPrimitive ((fun (callback,asyncState) -> ts.BeginGetPlaceFacts (p,callback,asyncState)), ts.EndGetPlaceFacts) let aGetPlaceWeb (city,state,country) = async { let p = new Place(City=city, State=state, Country=country) let! facts = ts.AsyncGetPlaceFacts(p) do savePlace facts return Complete(Web(facts.Center )) }
第二个表达式是 aGetPlaceFile,它也采用三个字符串来指示位置:
let aGetPlaceFile placeVals = async { let name = nameFromTriple placeVals if File.Exists(name) then let coord = coordFromFile name return Complete(File(coord)) else return Failure(Error) }
它会调用 nameFromTriple 来检索文件名,然后检查该文件是否存在(就像在 Web 调用检索到该位置时它要采取的操作一样)。因此在第一次调用任何位置时,aGetPlaceWeb 是唯一可用来检索信息的可行选择。在经过这次初始访问后,您即可从 aGetPlaceFile 检索相应信息。您可能已发现调用 aGetPlaceFile 要比调用 Web 快得多,因此应尽可能对其多加利用。而且还应该在其中包括超时机制,如果任何调用在指定时间内都未返回成功结果,则提供相应的消息。
创建一个位置列表 greatPlaces。然后,getWebPlaces 通过 greatPlaces 映射异步表达式 aGetPlaceWeb。也就是说,将对列表 greatPlaces 的每个元素都执行异步表达式 aGetPlaceWeb,并会返回异步表达式的列表(每个位置一个)。getFilePlaces 也同样如此,只不过它使用的是 aGetPlaceFile:
let greatPlaces = [ ("Austin","Texas","United States"); ("Las Vegas","Nevada","United States"); ("Asheville","North Carolina","United States"); ("Arlington","Virginia","United States"); ("Laguna Beach","California","United States")] let getWebPlaces = List.map aGetPlaceWeb greatPlaces let getFilePlaces = List.map aGetPlaceFile greatPlaces
在定义了这些位置及其后续检索表达式后,您需要将其混合在一起来创建一个完整列表。在该列表的每个元素中,无论是文件检索还是 Web 检索表达式都应异步执行。此时基元 first 就派上用场了。
使用 List.combine 函数将这些内容合并之后,每一对随后都被传递给基元 first 来创建异步表达式,它将对每一对都执行并行计算,并将第一个结果返回到终止行:
let combineAsyncFirst l1 l2 = List.map (fun (one,two) -> first [one;two]) (List.combine l1 l2) let combined = (combineAsyncFirst getWebPlaces getFilePlaces) let firstPlaces = List.map (terminateInSeconds 3 Timeout) combined let e = Async.Parallel firstPlaces let result = Async.Run e
列表 firstPlaces 就是与 terminateInSeconds 异步表达式合并的列表,它将在指定的时间后终止。在本例中,它将从可辨识联合 RetrievedValue 中返回 Timeout 值。
该组合所带来的机会在此段代码中得到了充分的体现。对于要在其中返回第一个成功终止(无论是由于速度原因还是因为出现错误)的计算对的列表而言,在命令式语言中创建它并不是一件可有可无的事。为其增加超时值(可最大程度缩短整个过程的时间)是一项至关重要的操作。但是,在创建进行更为复杂的异步计算时所需的所有小片段时,使用异步表达式以及组合可以简化操作。
下一行代码表明所有这些异步计算都将并行执行,您也因此将会从 Web 或文件系统(如果已经可用)中获取位置信息列表:
let e = Async.Parallel firstPlaces
图 4 表示此异步表达式所创建的计算模型。当框架返回两个并行执行 I/O 进程中的第一个进程之后,即可使用超时机制。然后可以再次使用这一完全相同的机制从两个冗余源并行检索数据。所有这些均被插入到一个并行执行各项 I/O 任务的更大框架中。
![](http://i.msdn.microsoft.com/cc967279.fig04(zh-cn).gif)
图 4 异步计算模型(单击图像可查看大图)
有人肯定会设想扩展这一组合并通过在并行机制和 first 机制之间重用框架来创建大型逻辑网络。倒计时机制显示了此体系结构的灵活程度以及它如何创建类似时钟的框架。并行操作与 first 机制的互动可创建相应的片段,而且您可以利用组合将其聚合在一起。
如果要求调用很可能会在以后被舍弃的结果似乎有些不合情理,这是要求 Web 服务执行可能毫无必要的工作。但是,在有些情况这样做却非常合理。(有关此问题的深入分析,请参阅本期的 End Bracket 专栏,网址为 msdn.microsoft.com/magazine/cc872850.aspx。)我在全球各地一些网络基础结构非常复杂而又不可靠的区域部署了软件。实际上就是在这些地方使我产生了有关这种异步组合的想法。在这些环境中拥有可靠的联网软件通常意味着可以调用多个冗余源的信息。
使用其他 .NET 语言
.NET Framework 拥有许多擅于描述不同事物的语言。F# 最强大的方面之一就是它可以创建能够从其他任何符合 .NET 的语言进行无缝调用的库,这是因为 F# 是一种面向对象的混合函数式语言。当然,这一功能是来之不易的。F# 库必须要经过仔细的设计,以使习惯于其他语言(如 C#)的开发人员可以直观掌握它。
例如,要为 C# 程序员创建合理的结构,我通常会将我的函数嵌入到一个类形式的体系结构中,它可以使 C# 开发人员感觉非常直观明了,即使基础代码是使用完全不同的语言所编写的:
type GreatPlaces() = let places = List<(string * string * string)>() member self.AddPlace(city,state,country) = places.Add( (city,state,country) ) member self.RetrievePlaces () = let lplaces = List.of_seq places let result = runPlaces lplaces Array.combine (Array.of_seq places) result |> Array.map compileResultToken
要在 F# 中设置 Web 服务调用,您可以使用命令行和自动代码生成工具。在 C# 等成熟的语言中,这些工具被集成到 Visual Studio 中,可以无缝地生成引用。在 F# 中,您只需几个简单步骤即可实现相同的结果。首先,打开 .NET 命令行工具并使用 wsdl.exe 解析 Web 服务 wsdl 并在 C# 中生成代理。然后,将该 C# 编译成 DLL 并在项目中进行引用。
例如,下面的脚本可用来与之前讨论的 TerraServer Web 服务进行通信:
> wsdl /namespace:WebService http://terraservice.net/terraservice.asmx Microsoft ® Web Services Description Language Utility Writing file 'c:\dev\TerraService.cs' >csc /target:library TerraService.cs
现在您已经生成了调用 Web 服务所必需的库(就像您处理任何类一样)。在 F# 源中,这两个步骤的后面会跟随一个引用指示符。在 F# 中,可以直接在文本中添加对外部库的引用,从而使您可以在调用 TerraService 库之前将其置于源文件中:
#r "TerraService.dll"
这就是众所周知的 .NET 功能,通过这种交互操作的程序,您可以使用最适合的语言解决每个相应的小问题。
Chance Coble, M.S. 是 Ideal Innovations Inc. 公司的一名科学家。几年前他从 Haskell 入手,逐渐培养了对函数式编程的兴趣,在过去的两年中,他一直致力于开发 F# 解决方案,以便为与机器学习、数据挖掘和数据集成等相关的全球生产提供解决方案。