异步表达式
本文介绍 F# 对异步表达式的支持。 异步表达式提供一种异步执行计算的方法,即,不阻塞其他工作的执行。 例如,异步计算可用于编写应用,这些应用的 UI 能够在应用程序执行其他工作时保持对用户的响应性。 借助 F# 异步工作流编程模型,可在将线程转换的详细信息隐藏在库中时编写功能程序。
也可以使用直接创建 .NET 任务的任务表达式来创作异步代码。 在与创建或使用 .NET 任务的 .NET 库进行广泛互操作时,首选任务表达式。 在使用 F# 编写大多数异步代码时,首选 F# 异步表达式,因为它们更简洁、更具组合性,并且避免了与 .NET 任务相关的某些警告。
语法
async { expression }
备注
在上面的语法中,由 expression
表示的计算设置为异步运行,即,在执行异步睡眠操作、I/O 和其他异步操作时不会阻塞当前计算线程。 异步计算通常在后台线程上启动,而当前线程上的工作继续执行。 表达式的类型为 Async<'T>
,其中 'T
是使用 return
关键字时表达式返回的类型。
Async
类提供支持多种方案的方法。 一般方法是创建 Async
对象来表示要异步运行的计算,然后使用某个触发函数启动这些计算。 所用触发取决于你是要使用当前线程、后台线程还是 .NET 任务对象。 例如,若要在当前线程上启动异步计算,可以使用 Async.StartImmediate
。 从 UI 线程启动异步计算时,不会阻塞处理用户操作(如击键和鼠标活动)的主事件循环,因此应用程序会保持响应状态。
使用 let! 进行异步绑定
在异步表达式中,有些表达式和操作是同步的,有些是异步的。 异步调用方法时,使用 let!
而不是普通的 let
绑定。 let!
的作用是在计算进行的过程中,使其他计算或线程上的执行得以继续。 在 let!
绑定的右侧返回后,异步表达式的其余部分恢复执行。
以下代码显示了 let
和 let!
之间的区别。 使用 let
的代码行仅将异步计算创建为一个对象,稍后可以使用 Async.StartImmediate
、Async.RunSynchronously
等方法运行该对象。 使用 let!
的代码行开始计算并执行异步等待:线程挂起,直到结果可用,此时继续执行。
// let just stores the result as an asynchronous operation.
let (result1 : Async<byte[]>) = stream.AsyncRead(bufferSize)
// let! completes the asynchronous operation and returns the data.
let! (result2 : byte[]) = stream.AsyncRead(bufferSize)
let!
只能用于直接等待 F# 异步计算 Async<T>
。 你可以间接等待其他类型的异步操作:
- .NET 任务、Task<TResult> 和非泛型 Task,通过与
Async.AwaitTask
结合使用 - .NET 值任务、ValueTask<TResult> 和非泛型 ValueTask,通过与
.AsTask()
和Async.AwaitTask
结合使用 - 遵循 F# RFC FS-1097 中指定的“GetAwaiter”模式的任何对象,通过与
task { return! expr } |> Async.AwaitTask
结合使用。
控制流
异步表达式可以包含控制流构造,如 for .. in .. do
、while .. do
、try .. with ..
、try .. finally ..
、if .. then .. else
和 if .. then ..
。 这些构造进而可能包含其他异步构造,但同步执行的 with
和 finally
处理程序除外。
F# 异步表达式不支持异步 try .. finally ..
。 对于这种情况,可以使用任务表达式。
use
和 use!
绑定
在异步表达式中,use
绑定可以绑定到 IDisposable 类型的值。 对于后者,处置清理操作是异步执行的。
除了 let!
,还可以使用 use!
执行异步绑定。 let!
和 use!
之间的区别与 let
和 use
之间的区别相同。 对于 use!
,在当前范围结束时释放对象。 请注意,在当前版本的 F# 中,虽然 use
允许将值初始化为 Null,但 use!
不允许。
异步基元
执行单个异步任务并返回结果的方法称为异步基元,它们是专门为与 let!
一起使用而设计的。 F# 核心库中定义了几个异步基元。 模块 FSharp.Control.WebExtensions
中定义了两种用于 Web 应用程序的此类方法:WebRequest.AsyncGetResponse
和 WebClient.AsyncDownloadString
。 给定 URL 后,这两个基元都从网页下载数据。 AsyncGetResponse
生成 System.Net.WebResponse
对象,AsyncDownloadString
生成表示网页 HTML 的字符串。
FSharp.Control.CommonExtensions
模块中包含几个用于异步 I/O 操作的基元。 System.IO.Stream
类的这些扩展方法为 Stream.AsyncRead
和 Stream.AsyncWrite
。
你还可以通过定义主体为异步表达式的函数或方法,来编写自己的异步基元。
若要在 .NET Framework 中使用为具有 F# 异步编程模型的其他异步模型设计的异步方法,请创建一个返回 F# Async
对象的函数。 F# 库具有简化此操作的函数。
此处包含一个使用异步表达式的示例;Async 类的方法的文档中还有许多其他示例。
此示例演示如何使用异步表达式并行执行代码。
在以下代码示例中,函数 fetchAsync
获取从 Web 请求返回的 HTML 文本。 fetchAsync
函数包含一个异步代码块。 对异步基元的结果(在本例中为 AsyncDownloadString
)进行绑定时,使用 let!
而不是 let
。
函数 Async.RunSynchronously
用于执行异步操作并等待其结果。 例如,可以通过将 Async.Parallel
函数与 Async.RunSynchronously
函数结合使用来并行执行多个异步操作。 Async.Parallel
函数采用 Async
对象列表,为要并行运行的每个 Async
任务对象设置代码,并返回表示并行计算的 Async
对象。 就像执行单个操作一样,调用 Async.RunSynchronously
开始执行。
runAll
函数并行启动三个异步表达式,并等待它们全部完成。
open System.Net
open Microsoft.FSharp.Control.WebExtensions
let urlList = [ "Microsoft.com", "http://www.microsoft.com/"
"MSDN", "http://msdn.microsoft.com/"
"Bing", "http://www.bing.com"
]
let fetchAsync(name, url:string) =
async {
try
let uri = new System.Uri(url)
let webClient = new WebClient()
let! html = webClient.AsyncDownloadString(uri)
printfn "Read %d characters for %s" html.Length name
with
| ex -> printfn "%s" (ex.Message);
}
let runAll() =
urlList
|> Seq.map fetchAsync
|> Async.Parallel
|> Async.RunSynchronously
|> ignore
runAll()