本文介绍 F# 中对任务表达式的支持,这些 表达式类似于异步表达式 ,但允许你直接创作 .NET 任务。 与异步表达式一样,任务表达式异步执行代码,也就是说,不会阻止执行其他工作。
异步代码通常使用异步表达式进行创作。 与创建或使用 .NET 任务的 .NET 库进行广泛互作时,首选使用任务表达式。 任务表达式还可以提高性能和调试体验。 但是,任务表达式附带了一些限制,本文稍后将介绍这些限制。
语法
task { expression }
在前面语法中,expression 所表示的计算被设置为运行 .NET 任务。 该任务在执行此代码后立即启动,并在当前线程上运行,直到执行其第一个异步操作(例如,异步睡眠、异步 I/O 或其他基本异步操作)。 表达式的类型是 Task<'T>,其中 'T 是使用 return 关键字时表达式返回的类型。
通过使用 let!进行绑定
在任务表达式中,某些表达式和作是同步的,有些是异步的。 等待异步操作的结果时,将使用 let,而不是普通 let! 绑定。
let! 的效果是在进行计算时,使执行能够继续处理其他计算或线程。
let!绑定右侧返回后,任务的其余部分将恢复执行。
以下代码显示了let和let!之间的区别。 使用 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 { } 表达式可以等待以下类型的异步操作:
- .NET 任务 Task<TResult> 和非泛型 Task任务。
- .NET 值任务 ValueTask<TResult> 和非泛型 ValueTask。
- F# 异步计算
Async<T>。 - 遵循 F# RFC FS-1097 中指定的“GetAwaiter”模式的任何对象。
return 表达式
在任务表达式中, return expr 用于返回任务的结果。
return! 表达式
在任务表达式中, return! expr 用于返回另一个任务的结果。 它等效于使用 let! 并立即返回结果。
控制流
任务表达式可以包括控制流构造for .. in .. do、while .. do、try .. with ..、try .. finally ..和if .. then .. elseif .. then ..。 这些构造可能又包括其他任务构造,但 with 和同步执行的 finally 处理程序除外。 如果需要异步try .. finally ..,请结合类型为use的对象一起使用IAsyncDisposable绑定。
use 和 use! 绑定
在任务表达式中,use 绑定可以绑定到类型为 IDisposable 或 IAsyncDisposable 的值。 对于后者,处置清理操作将异步执行。
除了 let!,你还可以使用 use! 来执行异步绑定。
let!和use!之间的差异与let和use之间的差异相同。
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 有多个重载,其中一个接受取消操作令牌。 如果不使用此重载,该特定的异步读取操作将不可取消。
有限并发和结果管道
若要限制一次启动的操作数,FSharp.Core 11 提供 Task.parallelLimit。 通过接受取消令牌的工厂,而不是已启动的任务:
open System.IO
open System.Threading
let readFiles (paths: seq<string>) (ct: CancellationToken) =
paths
|> Seq.map (fun path ->
fun token -> File.ReadAllTextAsync(path, token))
|> Task.parallelLimit 4 ct
结果数组保留输入顺序。 失败请求取消同级工作,并且每个操作必须观察其令牌。
Task.sequential 一次运行工厂一个; Task.parallelDoLimit 并 Task.sequentialDo 返回 unit 而不是数组。
有关Task.map和Task.bindTask.catch结果管道以及匹配的 Async/ValueTask 帮助程序,请参阅 Async 和任务函数。
后台任务
默认情况下,如果存在,则使用 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# 编译器编译为“可恢复状态机”。 这些内容在 可恢复代码 RFC 和 F# 编译器社区会话中进行了详细介绍。
运行时异步试验
生成器作者可以试验.NET运行时异步。 编译器生成的状态机通常存储等待后继续所需的状态。 使用运行时异步时,运行时会暂停并恢复特殊标记的方法。
注释
此原型使用 F# 11.2 功能。 在 .NET 11.0.100 SDK 中,启用<LangVersion>preview</LangVersion>包含运行时异步支持的编译器。 它还需要公开 MethodImplOptions.Async 和引用程序集的运行时和 System.Runtime.CompilerServices.AsyncHelpers.Await引用程序集。 引用程序集在编译时描述 API;正在运行的.NET运行时必须实现暂停/恢复行为。
示例的已验证组件组合为:
| 组分 | 版本或要求 |
|---|---|
| 编译器 | FSharp.Compiler.Service 43.13.101-rc2.26473.112提供的 F# 11 编译器。 |
| FSharp.Core | 包 11.0.101-rc2.26473.112,使用 lib/net10.0/FSharp.Core.dll。 此生成提供StateMachineHelpers.__runtimeAsyncReturn;netstandard2.0生成不提供。netstandard2.1 |
| Project目标 |
net11.0。 |
| 运行时和引用程序集 |
11.0.0-rc.1.26425.128.NET。 |
这些测试的组件不是单个 SDK 分发版。 .NET 10.0.12 公开引用 API,但暂停/恢复检查在其运行时失败,并带有 TypeLoadException。 使用上述已验证的运行时/引用组合;预览模式和 API 状态都不提供运行时支持。
以下调用站点使用应用程序定义的生成器异步读取文件:
let readManifest (path: string) =
runtimeTask {
let! text = System.IO.File.ReadAllTextAsync path
return text.Length
}
生成器将计算表达式连接到运行时异步返回内部函数和 await 帮助程序。 这不是一个新的内置 task 生成器:
open System.Threading.Tasks
open System.Runtime.CompilerServices
open Microsoft.FSharp.Core.CompilerServices
type RuntimeTaskBuilder() =
member inline _.Delay([<InlineIfLambda>] code: unit -> 'T) = code
member inline _.Run([<InlineIfLambda>] code: unit -> 'T) : Task<'T> =
StateMachineHelpers.__runtimeAsyncReturn (code())
member inline _.Return(value: 'T) = value
member inline _.Bind(work: Task<'T>, [<InlineIfLambda>] next: 'T -> 'U) =
next (AsyncHelpers.Await work)
let runtimeTask = RuntimeTaskBuilder()
__runtimeAsyncReturn 标记运行时异步的封闭方法。 在该方法中, AsyncHelpers.Await 可以在未完成的工作上挂起,并将控制权返回到调用方,然后继续处理结果。 这不是阻止 .Result 调用。
编译器检查支持的挂起模式;byrefs 无法在挂起期间保持实时状态。 该示例仅定义 Delay、 Run和 Return一个 Bind 重载,而不是具有取消、循环或异步处置的生产生成器。
启用在其中定义和扩展内联生成器的 F# 11.2 功能。 已编译 readManifest API 的调用方不需要它。 普通 task 表达式保留其实现。
AsyncHelpers.Await仅在运行时异步方法中使用;请参阅运行时异步规范。
完整的示例项目包括控制台入口点。 从其目录中,使用兼容的工具和要读取的文件运行 dotnet run -- manifest.txt 。 包含完全 manifest没有尾随换行符的文件将生成 8。 控制台入口点阻止完成; readManifest 本身返回 Task<int>。