任务表达式

本文介绍 F# 中对任务表达式的支持,这些 表达式类似于异步表达式 ,但允许你直接创作 .NET 任务。 与异步表达式一样,任务表达式异步执行代码,也就是说,不会阻止执行其他工作。

异步代码通常使用异步表达式进行创作。 与创建或使用 .NET 任务的 .NET 库进行广泛互作时,首选使用任务表达式。 任务表达式还可以提高性能和调试体验。 但是,任务表达式附带了一些限制,本文稍后将介绍这些限制。

语法

task { expression }

在前面语法中,expression 所表示的计算被设置为运行 .NET 任务。 该任务在执行此代码后立即启动,并在当前线程上运行,直到执行其第一个异步操作(例如,异步睡眠、异步 I/O 或其他基本异步操作)。 表达式的类型是 Task<'T>,其中 'T 是使用 return 关键字时表达式返回的类型。

通过使用 let!进行绑定

在任务表达式中,某些表达式和作是同步的,有些是异步的。 等待异步操作的结果时,将使用 let,而不是普通 let! 绑定。 let! 的效果是在进行计算时,使执行能够继续处理其他计算或线程。 let!绑定右侧返回后,任务的其余部分将恢复执行。

以下代码显示了letlet!之间的区别。 使用 let 的代码行实际上创建了一个任务对象,您可以稍后使用,例如 task.Wait()task.Result。 使用 let! 的代码行启动任务并等待其结果。

// let just stores the result as a task.
let (result1 : Task<int>) = stream.ReadAsync(buffer, offset, count, cancellationToken)
// let! completes the asynchronous operation and returns the data.
let! (result2 : int)  = stream.ReadAsync(buffer, offset, count, cancellationToken)

F# task { } 表达式可以等待以下类型的异步操作:

return 表达式

在任务表达式中, return expr 用于返回任务的结果。

return! 表达式

在任务表达式中, return! expr 用于返回另一个任务的结果。 它等效于使用 let! 并立即返回结果。

控制流

任务表达式可以包括控制流构造for .. in .. dowhile .. dotry .. with ..try .. finally ..if .. then .. elseif .. then ..。 这些构造可能又包括其他任务构造,但 with 和同步执行的 finally 处理程序除外。 如果需要异步try .. finally ..,请结合类型为use的对象一起使用IAsyncDisposable绑定。

useuse! 绑定

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

除了 let!,你还可以使用 use! 来执行异步绑定。 let!use!之间的差异与letuse之间的差异相同。 use! 对象在当前范围结束时被销毁。 请注意,在 F# 6 中, use! 不允许将值初始化为 null,即使 use 这样做也是如此。

open System
open System.IO
open System.Security.Cryptography
task {
    // use IDisposable
    use httpClient = new Net.Http.HttpClient()
    // use! Task<IDisposable>
    use! exampleDomain = httpClient.GetAsync "https://example.com/data.enc"
   
    // use IDisposable
    use aes = Aes.Create()
    aes.KeySize <- 256
    aes.GenerateIV()
    aes.GenerateKey()
    // do! Task
    do! File.WriteAllTextAsync("key.iv.txt", $"Key: {Convert.ToBase64String aes.Key}\nIV: {Convert.ToBase64String aes.IV}")

    // use IAsyncDisposable
    use outputStream = File.Create "secret.enc"
    // use IDisposable
    use encryptor = aes.CreateEncryptor()
    // use IAsyncDisposable
    use cryptoStream = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)
    // do! Task
    do! exampleDomain.Content.CopyToAsync cryptoStream
}

重要任务

值任务是用于避免基于任务的编程中的分配的结构。 值任务是一个短暂的值,通过使用 .AsTask() 转换为实际任务。

若要从任务表达式创建值任务,请使用 |> ValueTask<ReturnType>|> ValueTask。 例如:

let makeTask() =
    task { return 1 }

makeTask() |> ValueTask<int>

and! 绑定 (从 F# 10 开始)

在任务表达式中,可以同时等待多个异步作(Task<'T>ValueTask<'T>Async<'T>等)。 比较:

// We'll wait for x to resolve and then for y to resolve. Overall execution time is sum of two execution times.
let getResultsSequentially() =
    task {
        let! x = getX()
        let! y = getY()
        return x, y
    }

// x and y will be awaited concurrently. Overall execution time is the time of the slowest operation.
let getResultsConcurrently() =
    task {
        let! x = getX()
        and! y = getY()
        return x, y
    }

添加取消令牌和取消检查

与 F# 异步表达式不同,任务表达式不会隐式传递取消令牌,也不会隐式执行取消检查。 如果代码需要取消令牌,则应将取消令牌指定为参数。 例如:

open System.Threading

let someTaskCode (cancellationToken: CancellationToken) =
    task {
        cancellationToken.ThrowIfCancellationRequested()
        printfn $"continuing..."
    }

如果您打算使代码正确地可取消,请仔细检查是否将取消令牌传递给所有支持取消的 .NET 库操作。 例如,Stream.ReadAsync 有多个重载,其中一个接受取消操作令牌。 如果不使用此重载,该特定的异步读取操作将不可取消。

后台任务

默认情况下,如果存在,则使用 SynchronizationContext.Current .NET 任务进行计划。 这样,任务可以作为协作且交错运行的代理在用户界面线程上执行,而不会阻塞用户界面。 如果不存在,任务延续将安排到 .NET 线程池。

实际上,生成任务的库代码通常会忽略同步上下文,并根据需要始终切换到 .NET 线程池。 可以使用以下方法 backgroundTask { }实现此目的:

backgroundTask { expression }

后台任务在以下情况下忽略任何SynchronizationContext.Current:如果是在具有非 null SynchronizationContext.Current 的线程上启动的线程,则会使用 Task.Run 将其切换到线程池中的后台线程。 如果在具有 null SynchronizationContext.Current的线程上启动,它将在同一线程上执行。

注释

实际上,这意味着在 F# 任务代码中通常不需要调用 ConfigureAwait(false)。 相反,应在后台运行的任务应使用 backgroundTask { ... }。 对后台任务的任何外部任务绑定将在后台任务完成时对 SynchronizationContext.Current 进行重新同步。

有关 tailcalls 的任务限制

与 F# 异步表达式不同,任务表达式不支持尾调用。 也就是说,执行时 return! ,当前任务将注册为等待返回其结果的任务。 这意味着,使用任务表达式实现的递归函数和方法可能会创建未绑定的任务链,并且它们可能使用未绑定的堆栈或堆。 例如,考虑以下代码:

let rec taskLoopBad (count: int) : Task<string> =
    task {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! taskLoopBad (count-1)
    }

let t = taskLoopBad 10000000
t.Wait()

此编码样式不应与任务表达式一起使用,它将创建一个包含 10000000 个任务的链并导致一个 StackOverflowException。 如果在每次循环调用时添加异步操作,代码将使用无限堆。 请考虑切换此代码以使用显式循环,例如:

let taskLoopGood (count: int) : Task<string> =
    task {
        for i in count .. 1 do
            printfn $"looping... count = {count}"
        return "done!"
    }

let t = taskLoopGood 10000000
t.Wait()

如果需要异步尾调用,请使用支持尾调用的 F# 异步表达式。 例如:

let rec asyncLoopGood (count: int) =
    async {
        if count = 0 then
            return "done!"
        else
            printfn $"looping..., count = {count}"
            return! asyncLoopGood (count-1)
    }

let t = asyncLoopGood 1000000 |> Async.StartAsTask
t.Wait()

任务执行

任务是使用可恢复代码实现的,这是 F# 6 中的一项新功能。 任务由 F# 编译器编译为“可恢复状态机”。 这些内容在 可恢复代码 RFCF# 编译器社区会话中进行了详细介绍。

另请参阅