可以使用异步编程来避免性能瓶颈并增强应用程序的整体响应能力。 但是,编写异步应用程序的传统技术可能很复杂,因此难以编写、调试和维护。
Visual Studio 2012 引入了一种简化的方法(异步编程),利用 .NET Framework 4.5 及更高版本以及 Windows 运行时中的异步支持。 编译器执行开发人员用来执行的困难工作,并且应用程序保留类似于同步代码的逻辑结构。 因此,你只需付出一小部分精力即可获得异步编程的所有优势。
本主题概述了何时以及如何使用异步编程,并包含指向包含详细信息和示例的支持主题的链接。
异步可提高响应能力
异步对于可能导致阻塞的活动至关重要,例如当应用程序访问网络时。 有时,对 Web 资源的访问速度缓慢或延迟。 如果在同步进程中阻止此类活动,则整个应用程序必须等待。 在异步进程中,应用程序可以继续执行其他不依赖于 Web 资源的工作,直到潜在的阻塞任务完成。
下表显示了异步编程提高响应能力的典型区域。 .NET Framework 4.5 和 Windows 运行时中列出的 API 包含支持异步编程的方法。
应用程序区域 | 支持包含异步方法的 API |
---|---|
网络访问 | HttpClient、SyndicationClient |
使用文件 | StorageFile、StreamWriter、StreamReader、XmlReader |
使用映像 | MediaCapture、BitmapEncoder、BitmapDecoder |
WCF 编程 | 同步和异步作 |
Asynchrony 对于访问 UI 线程的应用程序尤其有价值,因为所有与 UI 相关的活动通常共享一个线程。 如果在同步应用程序中阻止了任何进程,则会阻止所有进程。 你的应用程序停止响应,你可能会认为它已失败,但实际上它可能只是正在等待。
使用异步方法时,应用程序将继续响应 UI。 例如,可以调整或最小化窗口的大小,或者如果不想等待应用程序完成,可以关闭应用程序。
基于异步的方法将自动传输的等效项添加到设计异步作时可以选择的选项列表。 也就是说,你可以获得传统异步编程的所有优势,但开发人员的工作量要少得多。
异步方法更易于编写
Visual Basic 中的 Async 和 Await 关键字是异步编程的核心。 通过使用这两个关键字,可以使用 .NET Framework 或 Windows 运行时中的资源创建异步方法,就像创建同步方法一样容易。 通过使用Async
和Await
定义的方法被称为异步方法。
以下示例演示异步方法。 代码里几乎所有内容都应该让你感到完全熟悉。 注释调出你添加的用来创建异步的功能。
可以在本主题末尾找到完整的 Windows Presentation Foundation (WPF) 示例文件,可以从 Async Sample:示例从“使用 Async 和 Await 进行异步编程”下载示例。
' Three things to note about writing an Async Function:
' - The function has an Async modifier.
' - Its return type is Task or Task(Of T). (See "Return Types" section.)
' - As a matter of convention, its name ends in "Async".
Async Function AccessTheWebAsync() As Task(Of Integer)
Using client As New HttpClient()
' Call and await separately.
' - AccessTheWebAsync can do other things while GetStringAsync is also running.
' - getStringTask stores the task we get from the call to GetStringAsync.
' - Task(Of String) means it is a task which returns a String when it is done.
Dim getStringTask As Task(Of String) =
client.GetStringAsync("https://learn.microsoft.com/dotnet")
' You can do other work here that doesn't rely on the string from GetStringAsync.
DoIndependentWork()
' The Await operator suspends AccessTheWebAsync.
' - AccessTheWebAsync does not continue until getStringTask is complete.
' - Meanwhile, control returns to the caller of AccessTheWebAsync.
' - Control resumes here when getStringTask is complete.
' - The Await operator then retrieves the String result from getStringTask.
Dim urlContents As String = Await getStringTask
' The Return statement specifies an Integer result.
' A method which awaits AccessTheWebAsync receives the Length value.
Return urlContents.Length
End Using
End Function
如果 AccessTheWebAsync
调用 GetStringAsync
和等待其完成之间没有任何工作,可以通过在以下单个语句中调用和等待来简化代码。
Dim urlContents As String = Await client.GetStringAsync()
以下特征总结了使上一个示例成为异步方法的内容:
方法签名包含
Async
修饰符。按照约定,异步方法的名称以“Async”后缀结尾。
返回类型是以下类型之一:
- 如果你的方法有操作数为 TResult 类型的返回语句,则为 Task(Of TResult)。
- Task 如果你的方法没有返回语句,或者只有没有操作数的返回语句。
- Sub:如果要编写异步事件处理程序。
有关详细信息,请参阅本主题后面的“返回类型和参数”。
该方法通常包含至少一个 await 表达式,该表达式标记出方法在等待的异步操作完成之前不能继续的时刻。 同时,将方法挂起,并且控件返回到方法的调用方。 本主题的下一节将解释悬挂点发生的情况。
在异步方法中,可使用提供的关键字和类型来指示需要完成的操作,且编译器会完成其余操作,其中包括持续跟踪控件以挂起方法返回等待点时发生的情况。 某些例程进程(如循环和异常处理)在传统异步代码中可能难以处理。 在异步方法中,可以像在同步解决方案中那样编写这些元素,并解决问题。
有关早期版本的 .NET Framework 中的异步的详细信息,请参阅 TPL 和传统 .NET Framework 异步编程。
异步方法中发生的情况
异步编程中要了解的最重要事项是控制流如何从方法移动到方法。 下图将引导你完成此过程:
关系图中的数字对应于以下步骤:
事件处理程序调用并等待
AccessTheWebAsync
异步方法。AccessTheWebAsync
创建一个 HttpClient 实例并调用 GetStringAsync 异步方法以字符串的形式下载网站的内容。在
GetStringAsync
中发生的某件事使其进度暂停。 可能必须等待网站下载或一些其他阻止活动。 为了避免阻止资源,GetStringAsync
将控制权交给调用方AccessTheWebAsync
。GetStringAsync
返回一个 Task(Of TResult),其中 TResult 是一个字符串,并将AccessTheWebAsync
任务getStringTask
分配给变量。 该任务表示调用GetStringAsync
的正在进行的过程,承诺在完成工作时生成实际字符串值。由于尚未等待
getStringTask
,因此,AccessTheWebAsync
可以继续执行不依赖于GetStringAsync
得出的最终结果的其他工作。 该工作由对同步方法DoIndependentWork
的调用表示。DoIndependentWork
是一种同步方法,用于执行其工作并返回到其调用方。AccessTheWebAsync
已运行完毕,可以不受getStringTask
的结果影响。AccessTheWebAsync
next 希望计算并返回下载的字符串的长度,但该方法在方法包含字符串之前无法计算该值。因此,
AccessTheWebAsync
使用 await 运算符暂停其进度,并将控制权传递给调用AccessTheWebAsync
的方法。AccessTheWebAsync
返回Task(Of Integer)
给调用方。 该任务承诺生成一个整数结果,该结果表示下载的字符串的长度。注释
如果
GetStringAsync
(因此getStringTask
)在AccessTheWebAsync
等待前完成,则控件会保留在AccessTheWebAsync
中。 如果所调用的异步进程(AccessTheWebAsync
)已经完成,并且 AccessTheWebSync 无需等待最终结果,那么暂停然后返回getStringTask
的费用就会被浪费。在调用方(此示例中的事件处理程序)中,处理模式继续。 调用方可能会在等待该结果之前执行其他不依赖于结果
AccessTheWebAsync
的工作,或者调用方可能会立即等待。 事件处理程序正在等待AccessTheWebAsync
,并且AccessTheWebAsync
正在等待GetStringAsync
。GetStringAsync
完成并生成字符串结果。 调用GetStringAsync
不会以预期的方式返回字符串结果。 (请记住,该方法已在步骤 3 中返回任务。相反,字符串结果存储在表示方法getStringTask
完成的任务中。 await 运算符从getStringTask
中检索结果。 赋值语句将检索的结果分配给urlContents
。如果
AccessTheWebAsync
字符串结果为字符串,该方法可以计算字符串的长度。 然后,AccessTheWebAsync
的工作也已完成,等待的事件处理程序可以继续运行。 在主题末尾的完整示例中,可以确认事件处理程序检索并打印长度结果的值。
如果你不熟悉异步编程,请花一分钟时间考虑同步和异步行为之间的差异。 同步方法在工作完成时返回(步骤 5),但异步方法在暂停工作时返回任务值(步骤 3 和步骤 6)。 当异步方法最终完成其工作时,任务将标记为已完成,结果(如果有)存储在任务中。
有关控制流的详细信息,请参阅异步程序中的控制流(Visual Basic)。
API 异步方法
你可能想知道在哪里可以找到支持异步编程的方法,例如GetStringAsync
。 .NET Framework 4.5 或更高版本中包含许多与 Async
和 Await
一起使用的成员。 可以通过成员名称附加的“Async”后缀和 TaskTask(Of TResult)返回类型来识别这些成员。 该 System.IO.Stream
类包含方法,如 CopyToAsync、ReadAsync 和 WriteAsync,以及同步方法 CopyTo、Read 和 Write。
Windows 运行时还包含许多可与 Windows 应用中的 Async
和 Await
一同使用的方法。 有关详细信息和示例方法,请参阅 在 C# 或 Visual Basic 中调用异步 API、 异步编程(Windows 运行时应用)和 WhenAny:在 .NET Framework 和 Windows 运行时之间进行桥接。
线程
异步方法旨在进行非阻塞操作。
Await
异步方法中的表达式不会在等待的任务运行时阻止当前线程。 相反,表达式将该方法的其余部分注册为延续,并将控制权返回到异步方法的调用方。
和Async
Await
关键字不会导致创建其他线程。 异步方法不需要多线程处理,因为异步方法不会在其自己的线程上运行。 该方法在当前同步上下文上运行,并且仅在方法处于活动状态时在线程上使用时间。 使用 Task.Run 可将 CPU 密集型任务转移到后台线程,然而,对于仅在等待结果可用的进程,后台线程并无帮助。
对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法优于 BackgroundWorker 用于 I/O 绑定操作,因为其代码更简单,无需防范竞争条件。 结合 Task.Run,异步编程在 CPU 绑定操作方面优于 BackgroundWorker,因为异步编程将运行代码的协调细节与传输到线程池的工作 Task.Run
分离。
异步和等待
如果使用 Async 修饰符指定方法是异步方法,则启用以下两项功能。
标记的异步方法可以使用 Await 来指定暂停点。 await 运算符告诉编译器,在等待的异步进程完成之前,异步方法无法继续超过该点。 同时,控件将返回到异步方法的调用方。
异步方法在
Await
表达式执行时暂停并不构成方法退出,只会导致Finally
代码块不运行。标记的异步方法本身可以通过调用它的方法等待。
异步方法通常包含运算符 Await
的一个或多个匹配项,但缺少 Await
表达式不会导致编译器错误。 如果异步方法不使用 Await
运算符标记挂起点,那么无论是否存在 Async
修饰符,该方法都将像同步方法一样执行。 编译器针对此类方法发出警告。
Async
和 Await
是上下文关键字。 有关详细信息和示例,请参阅以下主题:
返回类型和参数
在 .NET Framework 编程中,异步方法通常返回 Task 或 Task(Of TResult)。 在异步方法内部,Await
运算符被应用于从另一个异步方法调用中返回的任务。
如果方法包含一个Return语句,并且该语句需要一个类型的操作数,那么应指定TResult
作为返回类型。
如果方法没有 return 语句或者返回语句不返回操作数,则使用 Task
作为返回类型。
以下示例演示如何声明和调用返回 Task(Of TResult) 或 Task的方法:
' Signature specifies Task(Of Integer)
Async Function TaskOfTResult_MethodAsync() As Task(Of Integer)
Dim hours As Integer
' . . .
' Return statement specifies an integer result.
Return hours
End Function
' Calls to TaskOfTResult_MethodAsync
Dim returnedTaskTResult As Task(Of Integer) = TaskOfTResult_MethodAsync()
Dim intResult As Integer = Await returnedTaskTResult
' or, in a single statement
Dim intResult As Integer = Await TaskOfTResult_MethodAsync()
' Signature specifies Task
Async Function Task_MethodAsync() As Task
' . . .
' The method has no return statement.
End Function
' Calls to Task_MethodAsync
Task returnedTask = Task_MethodAsync()
Await returnedTask
' or, in a single statement
Await Task_MethodAsync()
每个返回的任务表示正在进行的工作。 任务可封装有关异步进程状态的信息,如果未成功,则最后会封装来自进程的最终结果或进程引发的异常。
异步方法也可以是方法 Sub
。 此返回类型主要用于定义事件处理程序,其中需要返回类型。 异步事件处理程序通常充当异步程序的起点。
无法等待作为 Sub
过程的异步方法,并且调用方捕获不到异步方法抛出的任何异常。
异步方法无法声明 ByRef 参数,但该方法可以调用具有此类参数的方法。
有关详细信息和示例,请参阅异步返回类型(Visual Basic)。 有关如何在异步方法中捕获异常的详细信息,请参阅 Try...Catch...Finally 语句。
Windows 运行时编程中的异步 API 具有以下返回类型之一,类似于任务:
- IAsyncOperation(Of TResult),对应于 Task(Of TResult)
- IAsyncAction,对应于 Task
- IAsyncActionWithProgress(Of TProgress)
- IAsyncOperationWithProgress(Of TResult, TProgress)
有关详细信息和示例,请参阅 在 C# 或 Visual Basic 中调用异步 API。
命名约定
按照约定,将“Async”追加到具有 Async
修饰符的方法的名称。
您可以忽略事件、基类或接口合同建议使用其他名称的惯例。 例如,不应重命名常见的事件处理程序,例如 Button1_Click
。
相关主题和示例 (Visual Studio)
完整的示例
以下代码是本主题讨论的 Windows Presentation Foundation (WPF) 应用程序中MainWindow.xaml.vb文件。 可以从 Async 示例下载示例:“使用 Async 和 Await 进行异步编程”的示例。
Imports System.Net.Http
' Example that demonstrates Asynchronous Programming with Async and Await.
' It uses HttpClient.GetStringAsync to download the contents of a website.
' Sample Output:
' Working . . . . . . .
'
' Length of the downloaded string: 39678.
Class MainWindow
' Mark the event handler with Async so you can use Await in it.
Private Async Sub StartButton_Click(sender As Object, e As RoutedEventArgs)
' Call and await immediately.
' StartButton_Click suspends until AccessTheWebAsync is done.
Dim contentLength As Integer = Await AccessTheWebAsync()
ResultsTextBox.Text &= $"{vbCrLf}Length of the downloaded string: {contentLength}.{vbCrLf}"
End Sub
' Three things to note about writing an Async Function:
' - The function has an Async modifier.
' - Its return type is Task or Task(Of T). (See "Return Types" section.)
' - As a matter of convention, its name ends in "Async".
Async Function AccessTheWebAsync() As Task(Of Integer)
Using client As New HttpClient()
' Call and await separately.
' - AccessTheWebAsync can do other things while GetStringAsync is also running.
' - getStringTask stores the task we get from the call to GetStringAsync.
' - Task(Of String) means it is a task which returns a String when it is done.
Dim getStringTask As Task(Of String) =
client.GetStringAsync("https://learn.microsoft.com/dotnet")
' You can do other work here that doesn't rely on the string from GetStringAsync.
DoIndependentWork()
' The Await operator suspends AccessTheWebAsync.
' - AccessTheWebAsync does not continue until getStringTask is complete.
' - Meanwhile, control returns to the caller of AccessTheWebAsync.
' - Control resumes here when getStringTask is complete.
' - The Await operator then retrieves the String result from getStringTask.
Dim urlContents As String = Await getStringTask
' The Return statement specifies an Integer result.
' A method which awaits AccessTheWebAsync receives the Length value.
Return urlContents.Length
End Using
End Function
Sub DoIndependentWork()
ResultsTextBox.Text &= $"Working . . . . . . .{vbCrLf}"
End Sub
End Class