培训
学习路径
F# 是一种开源的跨平台编程语言,通过它可轻松编写简洁、高效、可靠且实用的代码。 它是一种通用语言,可用于创建多种不同类型的应用程序,例如 Web API、桌面、IoT、游戏等。
由于各种各样的原因,异步编程成了新式应用程序必不可少的一种机制。 大多数开发人员会遇到以下两个主要用例:
尽管后台工作通常涉及多个线程的使用,但请务必分别考虑异步和多线程的概念。 事实上,它们是两个不同的关注点。异步并不意味着多线程,反之亦然。 本文更详细地描述了这两个不同的概念。
前一点(异步与多个线程的使用无关)值得进一步说明。 以下三个概念有时是相关的,但彼此完全独立:
这三个都是正交概念,但很容易混淆,尤其是一起使用时。 例如,你可能需要并行执行多个异步计算。 这种关系并不意味着并行或异步相互隐含。
以“异步”(asynchronous) 一词的词源为例,它涉及两个部分:
将这两个术语组合在一起后,你会发现“异步”的意思是“不同时”。 就这么简单! 这个定义并未隐含并发或并行。 在实践中也是如此。
实际上,F# 中的异步计算计划为独立于主程序流执行。 这种独立执行并不意味着并发或并行,也不意味着计算总是在后台进行。 事实上,异步计算甚至可以同步执行,这取决于计算的性质和计算的执行环境。
你应该了解的要点在于异步计算独立于主程序流。 虽然对异步计算的执行时间或方式没有什么保证,但有一些协调和计划异步计算的方法。 本文的其余部分将探讨 F# 异步的核心概念,以及如何使用 F# 内置的类型、函数和表达式。
在 F# 中,异步编程以两个核心概念为中心:异步计算和任务。
async { }
表达式的 Async<'T>
类型,表示可以启动以构成任务的可组合异步计算。task { }
表达式的 Task<'T>
类型,表示正在执行的 .NET 任务。一般情况下,如果正在与使用任务的 .NET 库交互,并且不依赖于异步代码尾调用或隐式取消令牌传播,则应考虑在新代码中使用 task {…}
而不是 async {…}
。
你可以在以下示例中看到“异步”编程的基本概念:
open System
open System.IO
// Perform an asynchronous read of a file using 'async'
let printTotalFileBytesUsingAsync (path: string) =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
printTotalFileBytesUsingAsync "path-to-file.txt"
|> Async.RunSynchronously
Console.Read() |> ignore
0
在该示例中,printTotalFileBytesUsingAsync
函数的类型为 string -> Async<unit>
。 调用该函数实际上不会执行异步计算。 相反,它会返回一个 Async<unit>
,作为要异步执行的工作的规范。 它在主体中调用 Async.AwaitTask
,将 ReadAllBytesAsync 的结果转换为适当的类型。
另一行重要代码是调用 Async.RunSynchronously
。 如果你想实际执行 F# 异步计算,这是你需要调用的异步模块启动函数之一。
这是与 C#/Visual Basic 样式的 async
编程的一个根本区别。 在 F# 中,可以将异步计算视为冷任务。 必须显式启动才能实际执行。 这种方式有一些优点,因为相比 C# 或 Visual Basic,它使你能够更轻松地对异步工作进行组合和排序。
以下示例在前一个示例的基础上对计算进行组合:
open System
open System.IO
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Parallel
|> Async.Ignore
|> Async.RunSynchronously
0
如你所见,main
函数还有更多元素。 从概念上讲,它执行以下操作:
Seq.map
将命令行参数转换为一系列 Async<unit>
计算。printTotalFileBytes
计算的 Async<'T[]>
。unit[]
)的 Async<unit>
。Async.RunSynchronously
显式运行整个组合计算,并阻塞线程直到该计算完成。运行此程序时,printTotalFileBytes
针对每个命令行参数并行运行。 因为异步计算独立于程序流执行,所以不会定义其打印信息和完成执行的顺序。 计算将并行计划,但不保证其执行顺序。
由于 Async<'T>
是一种工作规范,而不是已在运行的任务,因此你可以轻松执行更复杂的转换。 以下示例对一组异步计算进行排序,以便其逐个执行。
let printTotalFileBytes path =
async {
let! bytes = File.ReadAllBytesAsync(path) |> Async.AwaitTask
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
argv
|> Seq.map printTotalFileBytes
|> Async.Sequential
|> Async.Ignore
|> Async.RunSynchronously
|> ignore
这会将 printTotalFileBytes
计划为按 argv
的元素顺序执行,而不是计划为并行执行。 因为在前面的计算执行完成后才会计划每个后续操作,所以对计算排序时以执行时不会发生重叠为准。
在 F# 中编写异步代码时,通常会与负责计划计算的框架交互。 但是,情况并非总是如此,因此最好了解可用于计划异步工作的各种函数。
由于 F# 异步计算是一种工作规范,而不表示已在执行的工作,因此必须使用启动函数显式启动。 有许多异步启动方法在不同的情况下很有用。 以下部分介绍了一些较常见的启动函数。
在异步计算中启动子计算。 这允许并发执行多个异步计算。 子计算与父计算共享取消标记。 如果父计算被取消,子计算也会被取消。
签名:
computation: Async<'T> * ?millisecondsTimeout: int -> Async<Async<'T>>
使用场合:
需要注意的事项:
Async.StartChild
启动多个计算与将计算计划为并行执行不同。 如果要将计算计划为并行执行,请使用 Async.Parallel
。运行异步计算,并在当前操作系统线程上立即启动。 如果需要在计算期间更新调用线程上的某些内容,该函数很有用。 例如,如果异步计算必须更新 UI(例如更新进度栏),则应使用 Async.StartImmediate
。
签名:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
使用场合:
需要注意的事项:
Async.StartImmediate
。在线程池中执行计算。 返回 Task<TResult>,一旦计算终止(生成结果、引发异常或被取消),该类将在相应的状态下完成。 如果未提供取消标记,则使用默认取消标记。
签名:
computation: Async<'T> * ?taskCreationOptions: TaskCreationOptions * ?cancellationToken: CancellationToken -> Task<'T>
使用场合:
需要注意的事项:
Task
对象,如果经常使用它会增加开销。将一系列异步计算计划为并行执行,并按提供结果的顺序生成结果数组。 通过指定 maxDegreeOfParallelism
参数,可以选择优化/限制并行度。
签名:
computations: seq<Async<'T>> * ?maxDegreeOfParallelism: int -> Async<'T[]>
何时使用:
需要注意的事项:
将一系列异步计算计划为按其传递顺序执行。 此函数将执行第一个计算,然后执行下一个计算,依此类推。 不会并行执行任何计算。
签名:
computations: seq<Async<'T>> -> Async<'T[]>
何时使用:
需要注意的事项:
返回一个异步计算,该计算等待给定的 Task<TResult> 完成,并将其结果作为 Async<'T>
返回
签名:
task: Task<'T> -> Async<'T>
使用场合:
需要注意的事项:
创建一个异步计算,该计算执行给定的 Async<'T>
,并返回 Async<Choice<'T, exn>>
。 如果给定的 Async<'T>
成功完成,则返回包含结果值的 Choice1Of2
。 如果在完成之前引发异常,则返回包含引发的异常的 Choice2of2
。 如果将其用于本身由许多计算组成的异步计算,并且其中一个计算引发异常,则包含计算将完全停止。
签名:
computation: Async<'T> -> Async<Choice<'T, exn>>
使用场合:
需要注意的事项:
创建一个异步计算,该计算运行给定计算但删除其结果。
签名:
computation: Async<'T> -> Async<unit>
使用场合:
ignore
函数。需要注意的事项:
Async.Start
或其他需要 Async<unit>
的函数而必须使用 Async.Ignore
,请考虑是否可以放弃结果。 避免仅仅为了符合类型签名而放弃结果。运行异步计算并在调用线程上等待其结果。 如果计算生成异常,则传播该异常。 此调用将阻塞线程。
签名:
computation: Async<'T> * ?timeout: int * ?cancellationToken: CancellationToken -> 'T
何时使用:
需要注意的事项:
Async.RunSynchronously
会阻塞调用线程,直到执行完成。启动在线程池中返回 unit
的异步计算。 不等待其完成和/或观察异常结果。 使用 Async.Start
启动的嵌套计算独立于调用它们的父计算启动;其生存期与任何父计算无关。 如果父计算被取消,子计算不会被取消。
签名:
computation: Async<unit> * ?cancellationToken: CancellationToken -> unit
仅在以下情况下使用:
需要注意的事项:
Async.Start
启动的计算引发的异常不会传播给调用方。 调用堆栈将完全展开。Async.Start
启动的任何工作(例如调用 printfn
)都不会导致在程序执行的主线程上产生效果。如果使用 async { }
编程,你可能需要与使用 async/await 样式异步编程的 .NET 库或 C# 代码库进行互操作。 因为 C# 和大多数 .NET 库使用 Task<TResult> 和 Task 类型作为核心抽象,这可能会改变 F# 异步代码的编写方式。
一种选择是切换为直接使用 task { }
编写 .NET 任务。 也可以使用 Async.AwaitTask
函数来等待 .NET 异步计算:
let getValueFromLibrary param =
async {
let! value = DotNetLibrary.GetValueAsync param |> Async.AwaitTask
return value
}
可以使用 Async.StartAsTask
函数将异步计算传递给 .NET 调用方:
let computationForCaller param =
async {
let! result = getAsyncResult param
return result
} |> Async.StartAsTask
若要与使用 Task(即,不返回值的 .NET 异步计算)的 API 协同工作,你可能需要添加一个额外的函数,将 Async<'T>
转换为 Task:
module Async =
// Async<unit> -> Task
let startTaskFromAsyncUnit (comp: Async<unit>) =
Async.StartAsTask comp :> Task
已有一个接受 Task 作为输入的 Async.AwaitTask
。 借助此函数和之前定义的 startTaskFromAsyncUnit
函数,可以启动并等待 F# 异步计算中的 Task 类型。
在 F# 中,可以直接使用 task { }
编写任务,例如:
open System
open System.IO
/// Perform an asynchronous read of a file using 'task'
let printTotalFileBytesUsingTasks (path: string) =
task {
let! bytes = File.ReadAllBytesAsync(path)
let fileName = Path.GetFileName(path)
printfn $"File {fileName} has %d{bytes.Length} bytes"
}
[<EntryPoint>]
let main argv =
let task = printTotalFileBytesUsingTasks "path-to-file.txt"
task.Wait()
Console.Read() |> ignore
0
在该示例中,printTotalFileBytesUsingTasks
函数的类型为 string -> Task<unit>
。 调用该函数将开始执行任务。
调用 task.Wait()
会等待任务完成。
尽管整篇文章都提到了线程,但有两件重要的事情需要记住:
例如,计算实际上可能在其调用方的线程上运行,这取决于工作的性质。 计算也可以在线程之间“跳转”,在“等待”期间(例如传输网络调用时)临时借用它们执行有用的工作。
尽管 F# 提供了一些在当前线程上(或明确地不在当前线程上)启动异步计算的功能,但异步通常与特定的线程策略无关。
培训
学习路径
F# 是一种开源的跨平台编程语言,通过它可轻松编写简洁、高效、可靠且实用的代码。 它是一种通用语言,可用于创建多种不同类型的应用程序,例如 Web API、桌面、IoT、游戏等。