异步表达式

本文介绍 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! 绑定的右侧返回后,异步表达式的其余部分恢复执行。

以下代码显示了 letlet! 之间的区别。 使用 let 的代码行仅将异步计算创建为一个对象,稍后可以使用 Async.StartImmediateAsync.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 .. dowhile .. dotry .. with ..try .. finally ..if .. then .. elseif .. then ..。 这些构造进而可能包含其他异步构造,但同步执行的 withfinally 处理程序除外。

F# 异步表达式不支持异步 try .. finally ..。 对于这种情况,可以使用任务表达式

useuse! 绑定

在异步表达式中,use 绑定可以绑定到 IDisposable 类型的值。 对于后者,处置清理操作是异步执行的。

除了 let!,还可以使用 use! 执行异步绑定。 let!use! 之间的区别与 letuse 之间的区别相同。 对于 use!,在当前范围结束时释放对象。 请注意,在当前版本的 F# 中,虽然 use 允许将值初始化为 Null,但 use! 不允许。

异步基元

执行单个异步任务并返回结果的方法称为异步基元,它们是专门为与 let! 一起使用而设计的。 F# 核心库中定义了几个异步基元。 模块 FSharp.Control.WebExtensions 中定义了两种用于 Web 应用程序的此类方法:WebRequest.AsyncGetResponseWebClient.AsyncDownloadString。 给定 URL 后,这两个基元都从网页下载数据。 AsyncGetResponse 生成 System.Net.WebResponse 对象,AsyncDownloadString 生成表示网页 HTML 的字符串。

FSharp.Control.CommonExtensions 模块中包含几个用于异步 I/O 操作的基元。 System.IO.Stream 类的这些扩展方法为 Stream.AsyncReadStream.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()

另请参阅