使用异步编程可以避免性能瓶颈,提高应用程序的整体响应速度。然而,传统的编写异步应用程序的技术可能会很复杂,使其难以编写、调试和维护。
C# 支持简化的方法,即异步编程,它利用了 .NET 运行时(runtime)的异步支持。编译器完成了开发人员过去要做的艰巨工作,而你的应用程序则保留了类似同步代码的逻辑结构。因此,你只需做一小部分工作就可以获得异步编程的所有好处。
本文概述了何时以及如何使用异步编程,并包含指向详细信息和示例的链接。
异步提升响应能力#
异步对于可能阻塞的活动非常重要,比如网络访问。访问网络资源有时会很慢或延迟。如果这样的活动在同步过程中被阻塞,整个应用程序必须等待。而在异步过程中,应用程序可以继续进行其他不依赖于网络资源的工作,直到可能阻塞的任务完成。
下表显示了异步编程改善响应性能力的典型领域。列出了来自 .NET 和 Windows Runtime 的 APIs 包含支持异步编程的方法。
异步对于访问 UI 线程的应用程序尤为重要,因为所有与 UI 相关的活动通常共享一个线程。如果同步应用程序中的任何进程被阻塞,那么所有进程都会被阻塞。您的应用程序停止响应,并且你可能会认为它已经失败了,而实际上它只是在等待。
当你使用异步方法时,应用程序将继续响应 UI 操作。例如,你可以调整窗口大小或最小化窗口,或者如果不想等待它完成,则可以关闭该应用程序。
在设计异步操作时,基于异步的方法在选项列表中添加了相当于自动传输的选项。也就是说,开发人员只需要投入较少的工作量即可获得传统异步编程的所有优点。
异步方法易于编写(Async methods are easy to write)#
使用 C# 中的 async 和 await 关键字是异步编程的核心。通过使用这两个关键字,您可以使用 .NET Framework、.NET Core 或 Windows Runtime 中的资源来创建异步方法,就像创建同步方法一样简单。使用 async 关键字定义的异步方法称为 async 方法。
下面的示例展示了一个 async 方法。
你可以从 Asynchronous programming with async and await in C# 下载完整的 Windows Presentation Foundation(WPF)示例。
public async Task<int> GetUrlContentLengthAsync()
{
var client = new HttpClient();
Task<string> getStringTask =
client.GetStringAsync("https://learn.microsoft.com/dotnet");
DoIndependentWork();
string contents = await getStringTask;
return contents.Length;
}
void DoIndependentWork()
{
Console.WriteLine("Working...");
}
你可以从前面的示例中学到几个实践方法。首先是方法签名,它包括 async
修饰符。返回类型是 Task<int>
(有关更多选项,请参阅 “Return Types” 部分)。方法名称以 Async 结尾。在方法体中,GetStringAsync
返回一个 Task<string>
。这意味着当你等待任务完成时,会得到一个字符串(内容)。在等待 Task 之前,可以执行不依赖于 GetStringAsync
返回的字符串的工作。
请特别注意 await
操作符:
GetUrlContentLengthAsync
无法继续执行直到getStringTask
完成。- 同时,控制权回归
GetUrlContentLengthAsync
的调用者。 - 当
getStringTask
完成后,在此处恢复控制。 - 然后,
await
操作符从getStringTask
检索字符串结果。
return 语句指定了一个整数结果。任何正在等待 GetUrlContentLengthAsync
的方法都会获取长度值。
如果 GetUrlContentLengthAsync
在调用 GetStringAsync
和等待其完成之间没有其他工作可做,则可以通过以下单个语句来简化代码:
string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");
以下特点总结了前面示例中的异步方法:
- 方法签名包含
async
修饰符。 - 异步方法的名称通常以 "Async" 后缀结尾。
- 返回类型可以是以下类型之一:
-
如果方法中有返回语句且操作数的类型为
TResult
,则返回 Task。 -
如果方法没有返回语句或者返回语句没有操作数,则返回 Task。
-
如果编写异步事件处理程序,则返回
void
。 -
其他具有
GetAwaiter
方法的任何其他类型。更多信息,请参阅 Return types and parameters 部分。
-
- 该方法通常包含至少一个
await
表达式,它标记了一个点,在该点上,直到等待的异步操作完成方法才能继续。同时,该方法被挂起,并将控制权返还给调用方。本文的下一节将说明在挂起点会发生什么。
在异步方法中,使用提供的关键字和类型来指示要做什么,而编译器则负责其余工作,包括跟踪当控制权返回到挂起状态下的 await 点时必须发生的事情。一些常规流程(例如循环和异常处理)在传统异步代码中可能难以处理。但在异步方法中,你可以像在同步解决方案一样编写这些元素,问题也就迎刃而解了。
若要详细了解旧版 .NET Framework 中的异步性,请参阅 TPL 和传统 .NET Framework 异步编程。
异步方法的运行机制(What happens in an async method)#
异步编程中最重要的是理解控制流(control flow)如何从一个方法移动到另一个方法。下图将引导你完成这个过程:
当调用方调用异步方法时,图中的数字与以下步骤相对应:
-
调用方调用并等待
GetUrlContentLengthAsync
异步方法。 -
GetUrlContentLengthAsync
创建一个 HttpClient 实例,并调用GetStringAsync
异步方法以字符串形式下载网站内容。 -
在
GetStringAsync
中发生了一些挂起进度(progress)的事情。可能需要等待网站下载或其他阻塞活动。为避免阻塞资源,GetStringAsync
将控制权交给(yield control)其调用者GetUrlContentLengthAsync
。
GetStringAsync
返回一个 Task,其中TResult
是一个字符串,并且GetUrlContentLengthAsync
将任务分配给getStringTask
变量。该任务表示对GetStringAsync
的调用正在进行中,承诺在工作完成时产生实际的字符串值。 -
由于尚未 await
getStringTask
,GetUrlContentLengthAsync
可以继续进行其他工作,而不依赖于GetStringAsync
的最终结果。这些工作(继续进行其他工作)通过调用同步方法DoIndependentWork
来表示。 -
DoIndependentWork
是一个同步方法,它完成工作后返回给调用者。 -
GetUrlContentLengthAsync
已经没有在getStringTask
结果出来前需要完成的工作了。接下来,GetUrlContentLengthAsync
希望计算并返回已下载字符串的长度,但是在获取到该字符串之前无法计算该值。
因此,GetUrlContentLengthAsync
使用 await 运算符挂起进度,并将控制权交给调用GetUrlContentLengthAsync
的方法。GetUrlContentLengthAsync
向调用者返回一个Task<int>
。该任务表示承诺产生一个整数结果,即下载字符串的长度。在调用方内部,处理模式仍在继续。在等待
GetUrlContentLengthAsync
的结果之前,调用者可能会执行不依赖于GetUrlContentLengthAsync
结果的其他工作,或者调用者可能会立即等待。调用方正在等待GetUrlContentLengthAsync
,而GetUrlContentLengthAsync
正在等待GetStringAsync
。 -
GetStringAsync
完成并生成一个字符串结果。所返回的字符串结果并不是你所期望的那样。(请记住,在第 3 步中,该方法已经返回了一个 Task)。相反,字符串结果存储在代表方法完成的任务getStringTask
中。await 运算符从getStringTask
取回结果。赋值语句将取回到的结果赋值给contents
变量。 -
当
GetUrlContentLengthAsync
获得字符串结果时,该方法可以计算出字符串长度。然后,GetUrlContentLengthAsync
也完成了工作,并且等待事件处理程序可以恢复执行。
在本文末尾提供完整示例中,可以确认事件处理程序取回和打印长度的值。如果你刚开始接触异步编程,请花一分钟时间考虑同步和异步行为之间的区别。同步方法在其工作完成时返回(第 5 步),但异步方法在其工作暂停时返回任务值(第 3 和 6 步)。当异步方法最终完成其工作时,任务被标记为已完成,而结果(如果有)将存储在任务中。
API 异步方法(API async methods)#
.NET Framework 4.5+ 和 .NET Core 包含许多支持异步编程的方法,可以通过成员名称后缀 "Async" 以及返回类型为 Task 或 Task 来识别它们。例如,System.IO.Stream
类中有CopyToAsync、ReadAsync 和 WriteAsync 等方法,以及同步方法 CopyTo、Read 和 Write。
Windows Runtime 还包含许多可在 Windows 应用程序中使用 async
和 await
的方法。有关更多信息,请参阅 UWP 开发的线程和异步编程以及 Asynchronous programming (Windows Store apps) 和 Quickstart: Calling asynchronous APIs in C# or Visual Basic(如果你使用较早版本的 Windows Runtime)。
线程(Thread)#
异步方法旨在实现非阻塞(non-blocking)操作。当等待的任务正在运行时,**await
表达式不会阻塞当前线程 **。相反,表达式会将方法的其余部分标记为继续,并将控制权返回给 async 方法的调用者。
**async
和 await
关键字不会创建额外的线程。异步方法不需要多线程,因为它不在自己的线程上运行 **。该方法在当前同步上下文(context)中运行,并且仅当该方法处于活动状态时才使用线程上的时间。可以使用 Task.Run 将 CPU-bound 工作移至后台线程,但后台线程对于等待结果可用性的进程没有帮助。
与现有方案相比,在几乎所有情况下,基于 async 的异步编程方式更可取。特别是对于 I/O-bound 操作来说,异步方法优于 BackgroundWorker 类,因为代码更简单且无需防止竞争条件发生。结合 Task.Run 方法使用时,异步编程也优于 BackgroundWorker 类进行 CPU-bound 操作,因为异步编程将运行代码的协调细节与 Task.Run
转移到线程池的工作分离开来。
async 和 await#
如果使用 async 修饰符指定一个方法为异步方法,就会启用以下两种功能:
-
被标记的 async 方法可以使用 await 来指定挂起点。
await
操作符会告诉编译器,在等待的异步进程完成之前,异步方法不能在该挂起点后继续执行。在此期间,控制权返回给异步方法的调用者。在
await
表达式处挂起的异步方法并不会造成方法退出,而且 finally 块也不会运行。 -
被标记的 async 方法本身,可以被调用它的方法等待。
一个 async 方法通常包含一个或多个 await
操作符,但如果没有 await
表达式,编译器也不会报错(只是发出警告)。如果一个 async 方法没有使用 await
操作符来标记挂起点,那么尽管有 async
修饰符,该方法还是会像同步方法一样执行。编译器会对此类方法发出警告,强烈建议不要这样做,因为这样的代码非常低效。
async
和 await
是上下文关键字。有关详细信息和示例,请参阅以下主题:
返回类型和参数(Return types and parameters)#
在异步方法中,通常返回一个 Task 或 Task。在异步方法内部,await
操作符用作从另一个异步方法调用后返回的 Task。
如果该方法包含指定 TResult
类型的操作数的 return 语句,则将 Task 指定为返回类型。
如果该方法没有 return 语句或者包含不返回操作数的 return 语句,则使用 Task 作为返回类型。
还可以指定任何其他返回类型,只要该类型包含 GetAwaiter
方法即可。ValueTask 就是这种类型,在 System.Threading.Tasks.Extension NuGet 软件包中提供。
以下示例显示了如何声明和调用返回 Task 或者 Task 的方法:
async Task<int> GetTaskOfTResultAsync()
{
int hours = 0;
await Task.Delay(0);
return hours;
}
Task<int> returnedTaskTResult = GetTaskOfTResultAsync();
int intResult = await returnedTaskTResult;
// Single line
// int intResult = await GetTaskOfTResultAsync();
async Task GetTaskAsync()
{
await Task.Delay(0);
// No return statement needed
}
Task returnedTask = GetTaskAsync();
await returnedTask;
// Single line
await GetTaskAsync();
每个返回的 Task
代表着正在进行的工作。Task
封装了异步过程状态的信息,最终可能是来自该过程的最终结果或者该过程引发的异常(如果不成功)。
异步方法也可以有 void
返回类型。这种返回类型主要用于定义事件处理程序(event handler),其需要 void 返回类型。异步事件处理程序通常作为异步程序的起点。
具有 void
返回类型的异步方法不能被等待,并且调用者无法捕获该方法抛出的任何异常。
异步方法不能声明 in、ref 或 out 参数,但可以调用具有此类参数的方法。同样地,异步方法不能通过引用返回值,但可以调用具有 ref 返回值的方法。
更多信息和示例,请参阅 Async return types (C#)。
Windows runtime 编程中的异步 API 具有下列返回类型之一(类似于 Task):
- IAsyncOperation, 对应 Task
- IAsyncAction, 对应 Task
- IAsyncActionWithProgress
- IAsyncOperationWithProgress<TResult,TProgress>
命名约定(Naming convention)#
按照惯例,返回常见可等待类型的方法(例如 Task
、Task<T>
、ValueTask
、ValueTask<T>
)应该以 "Async" 结尾。开始异步操作但不返回可等待类型的方法不应该以 "Async" 结尾命名,但可以以 "Begin"、"Start" 或其他动词开头来暗示此方法不会返回或抛出操作结果。
如果事件(event)、基类(base class)或接口契约(interface contract)建议使用不同的名称,则可以忽略该约定(以 “Async” 结尾)。例如,您不应重命名常见的事件处理程序,如 OnButtonClick
。