13.1.2 异步下载网页
在我们使用异步工作流来获取网页内容之前,需要引用 FSharp.PowerPack.dll 库,它包含许多 .NET 方法的异步版本。当开发独立的应用程序时,可以使用添加引用命令。在这一章中,我们将使用互动开发模式,创建一个新的 F# 脚本文件,使用 #r 指令(清单 13.1)。
Listing 13.1 Writing code using asynchronous workflows (F# Interactive)
> #r "FSharp.PowerPack.dll";;
> open System.IO
open System.Net;;
> let downloadUrl(url:string) = async {
let request = HttpWebRequest.Create(url)
let! response = request.AsyncGetResponse()
use response = response
let stream = response.GetResponseStream()
use reader = new StreamReader(stream)
return! reader.AsyncReadToEnd() };;
val downloadUrl : string -> Async<string>
打开所需的所有命名空间后, 我们定义一个函数,它使用异步工作流程实现,用 async 值作为计算生成器。可以轻松地证明,它就是一个普通的值;在 Visual Studio 中,如果在值之后立即键入一个点 (.),智能感知会显示它包含的所有常用的计算生成器的成员,比如,Bind 和 Return,以及几个其他的基元,我们在以后会需要。打印的类型签名显示,计算类型是 Async<string>。后面,我们会详细讨论这个类型。
清单 13.1 中的代码,在执行由 F# 库所提供的异步操作 AsyncGetResponse 基元时,使用 let! 结构。这个方法的返回类型是 Async<WebResponse>,所以,let! 结构组合了两个异步操作,把实际的 WebResponse 值绑定到符号 response 上。这意味着,一旦异步操作完成后,我们就可以使用这个值。
在下一行使用到了use 基元,给定对象的一旦超出范围,就被释放。我们已经讨论过,在普通的 F# 程序环境中的 use,和在异步工作流内部的行为是类似的。工作流完成时,它将释放 HTTP 响应。我们使用值隐藏(value hiding)去隐藏原来的 response 符号,并声明一个新的,它将被释放。这是一种常见的模式,所以,F# 提供一种简便的方法,使用 use! 基元来写,它简单地组合了 let! 和 use。现在,我们知道这件事,可以把上面的两行替换成:
use! response = request.AsyncGetResponse()
在清单 13.1 的最后一行,我们使用了基元 return!,之前从没见过,它可以运行另一个异步操作(就像使用 let! 基元),只是当操作完成时,返回结果,而不是在赋值给符号时。像 do! 基元一样,这是简单的语法糖(syntactic sugar)。计算生成器不需要实现任何其他的成员 ,编译器就可以把代码看作是这样写的(实际的转换更简单):
let! text = reader.AsyncReadToEnd()
return text
现在,我们已经有了创建异步计算的 downloadUrl 函数,还应该确定如何可以用它来下载网页的内容。你可以在清单 13.2 中看到,可以使用 Async 模块中的函数来执行工作流。
Listing 13.2 Executing asynchronous computations (F# Interactive)
> let downloadTask = downloadUrl("http://www.manning.com");;
val downloadTask : Async<string>
> Async.RunSynchronously(downloadTask);;
val it : string = "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0
Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-tr
ansitional.dtd"><html><head> (...)"
> let tasks =
[ downloadUrl("http://www.tomasp.net");
downloadUrl("http://www.manning.com") ];;
val tasks : list<Async<string>>
> let all = Async.Parallel(tasks);;
val all : Async<string[]>
> Async.RunSynchronously(all);;
val it : string[] = [ "..."; "..." ]
使用异步工作流写的代码是被延迟的,这意味着,当我们执行第一行的 downloadUrl 函数,它还不会开始下载网页。返回的值 (Async<string> 类型)表示想要运行的计算,就像函数值表示以后可以执行的代码一样。Async 模块提供了运行工作流方法,表 13.1 描述了其中一部分。
表 13.1 在标准 F# 库 Async 模块中,可用的处理异步工作流的基元
Primitive | Type of primitive and description |
RunSynchronously | Async<'T> –> 'T 在当前纯种上启动级定的工作流。在这个工作流中使用异步操作时,工作流恢复在线程上的用于调用异步回调。此操作会阻塞调用者线程,并等待这个工作流的结果。 |
Start | Async<unit> –> unit 在后台(使用线程池线程) 启动给定的工作流,并立即返回。工作流与随后的调用者代码并行执行。如在签名中提示的,工作流不返回值。 |
CreateAsTask | Async<'T> -> Task<'T> |
Parallel | seq<Async<'a>> -> Async<array<'a>> 取一个异步工作流的集合,并返回一个工作流,它以并行方式执行所有参数值。返回的工作流等待所有操作完成,然后,在一个数组中返回它们的结果。 |
在清单 13.2 中,我们最初使用 Async.RunSynchronously,阻塞了调用线程,这对于以交互方式测试工作流,是有用的。在下一步,我们创建工作流值的列表。此时,什么也没有启动。一旦我们有了集合,就可以使用 Async.Parallel 方法构建一个工作流,将并行执行列表中的所有工作流。这里,仍不执行任何原始的工作流。要做到这一点,需要再次使用 Async.RunSynchronously,启动组合的工作流,并等待结果。组合的工作流启动所有工作流,并等待所有都完成。
代码仍阻塞,等待整体结果,但它有效地运行。它使用 .NET 线程池来平衡运行的线程的最大数目。如果我们创建几百个任务,它不会创建几百个线程,因为,这样做效率不高。而是使用数量较少的线程。当工作流使用 let! 结构, 到达基元的异步操作调用,它在系统中注册一个回调,并释放这个线程。因为 .NET 使用线程池管理线程,完成了工作的线程可以重用,以启动另一个异步工作流。当我们使用异步工作流时,并行运行的任务数量可以明显大于直接使用的线程数。
在本章中,我们需要交互地获取数据,因此,我们感兴趣的是,并行运行的工作流,而不是开发响应的图形用户界面应用程序。后一类应用程序(也称为有反映的应用程序(reactive applications)) 是重要的,第 16 章将关注这个主题。现在,我们已经看到了使用异步工作流的代码表象,下面,就看看它们是如何实现的。