13.1.3 理解工作流的原理
从前一章我们知道,用计算表达式写的 F# 代码,会由对应的计算生成器,转换成使用基本操作的表达式。对于异步工作流来说,let! 结构转换成对 async.Bind 的调用,return 转换成 async.Return;此外,异步工作流自动延迟,因此,计算本身需要包装成另外的基本操作,以确保整个代码包含在一个函数中,这个函数能够在后面工作流启动时执行。清单 13.3 是清单 13.2 工作流转换后的版本。
清单13.3 显式构造异步工作流 (F#)
async.Delay(fun () –>
let request = HttpWebRequest.Create(url)
async.Bind(request.AsyncGetResponse(), fun response –>
async.Using(response, fun response –>
let stream = response.GetResponseStream()
async.Using(new StreamReader(stream), fun reader –>
reader.AsyncReadToEnd() )
)
)
)
Delay 成员把函数包装成可以延迟执行的工作流。这个 Lambda 函数的主体就作为一个参数值,创建 HTTP 请求,使用自定义的异步值绑定,把值指定给 resp 符号。编译器把每个 use 绑定转换成对 Using 成员的调用,这是另一个基本操作,由计算表达式生成器提供的。它负责在工作流结束时,释放对象,不管成功与否。
Delay 成员是计算生成器成员之一,在实现计算表达式时提供。在清单 13.3 中,它把函数为参数,返回异步工作流(类型是 unit ->Async<’a>),返回的工作流,是包装了这个函数的值(Async<’a>)。有了这个基本操作,整个计算可以包含在一个函数中,在创建 Async<’a> 值时并不执行。这与前一章关于 option<’a> 类型的例子,完全不同。选项表示值,所以,计算表达式立即运行,并返回新的选项值;而工作流表示计算。当我们深入了解 Async<’a> 类型之后,就会更清楚其意思。
在清单 13.3 中的另一个基本操作是 Bind 成员。从前一章我们知道,这是所有计算表达式的核心。在异步工作流中,Bind 可以启动操作,而不阻塞调用者线程。我们使用基本操作,比如 Async.RunSynchronously,执行工作流时,到底会发生什么:
1、作为参数值传递给 Delay 基本操作的函数,启动执行,同步创建对象,表示对给定URL 的 HTTP 请求。
2、调用 AsyncGetResponse 的结果,是异步工作流的基本操作,它知道如何启动请求,操作完成时调用指定函数。
3、执行 Bind 成员,把第二步中的工作流作为第一个参数,另一个参数是函数,参数为 HTTP 响应,即工作流完成时应该执行的操作。这个函数称为连续(continuation),这个术语我们已经见过(在第十章讨论递归时,我们用它作为累加器参数,累加需要在后面运行的代码)。
4、Bind 成员运行由 AsyncGetResponse 创建的工作流,把连续作为参数。然后,工作流基本操作调用 .NET 的 BeginGetResponse 方法,指示系统启动下载响应,当操作完成时调用给定连续。在这里,Bind 成员返回,正在执行操作的线程被释放,可以继续做其它工作,或者返回给线程池。
5、当响应准备就绪时,系统将调用连续。工作流使用 .NET 方法 EndGetResponse 获得响应对象,执行提供给 Bind 成员的连续,表示工作流的其余部分。注意,系统再从线程池取得线程,因此,每次使用 let! 基本操作时,工作流的其余部分可能运行在不同的线程上。
执行异步工作流,不要等待结果,更重要在于,把连续作为参数值;当工作流中的相应步骤完成时,这个连续就会执行。使用异步工作流,不必显式写使用连续的代码,编译器会把 let! 基本操作转换成对 Bind 成员的调用,自动创建连续。
异步工作流的类型
使用异步工作流,不一定非要了解所有的详节,而可能对它是如何实现的,会有点兴趣。我们已经知道,异步工作流类似于函数,表示延迟执行的计算。在 F# 库中,它的类型用函数表示;这个类型有点复杂,最简单的异步计算可能像下面这样:
type Async<’T> = ((‘T -> unit) * (exn -> unit) * (unit -> unit)) –> unit
这是一个函数,有三个参数组成元组,返回 unit 值。这三个参数很重要,因为它们是连续函数,在异步工作流完成时调用。第一个参数是成功时连续,类型为 ‘T -> unit,表示把工作流的结果作为参数,当工作流完成时,将调用这个连续,然后,它可以运行其他工作流,或者任何其他代码。第二个参数是异常时连续,参数为 exn 值,这是 F# 对 .NET Exception 类型的缩写,可以猜到,它是工作流执行失败时的操作。第三个函数为取消时连续,工作流被取消时触发。
虽然不需要完全了解异步工作流的实现细节,但是,能够创建自定义的工作流基本操作,相当于清单 13.3 中使用的 AsyncGetResponse 方法,还是非常有用的。然后,可以使用其余的构件(building blocks),不费什么劲就能异步运行自己的代码了。