任务异步编程模型

可以使用异步编程来避免性能瓶颈并增强应用程序的整体响应能力。 但是,编写异步应用程序的传统技术可能很复杂,因此难以编写、调试和维护。

C# 支持简化的方法、异步编程,利用 .NET 运行时中的异步支持。 编译器执行开发人员用来执行的困难工作,并且应用程序保留类似于同步代码的逻辑结构。 因此,你只需付出一小部分精力即可获得异步编程的所有优势。

本主题概述了何时以及如何使用异步编程,并包含指向包含详细信息和示例的支持主题的链接。

异步可提高响应能力

异步性对于可能会导致阻塞的活动(例如 Web 访问)至关重要。 有时,对 Web 资源的访问速度缓慢或延迟。 如果在同步进程中阻止此类活动,则整个应用程序必须等待。 在异步进程中,应用程序可以继续执行其他不依赖于 Web 资源的工作,直到潜在的阻塞任务完成。

下表显示了异步编程提高响应能力的典型区域。 .NET 和 Windows 运行时中列出的 API 包含支持异步编程的方法。

应用程序区域 使用异步方法的 .NET 类型 使用异步方法的 Windows 运行时类型
网络访问 HttpClient Windows.Web.Http.HttpClient
SyndicationClient
使用文件 JsonSerializer
StreamReader
StreamWriter
XmlReader
XmlWriter
StorageFile
使用映像 MediaCapture
BitmapEncoder
BitmapDecoder
WCF 编程 同步和异步作

Asynchrony 对于访问 UI 线程的应用程序尤其有价值,因为所有与 UI 相关的活动通常共享一个线程。 如果在同步应用程序中阻止了任何进程,则会阻止所有进程。 你的应用程序停止响应,你可能会认为它已失败,但实际上它可能只是正在等待。

使用异步方法时,应用程序将继续响应 UI。 例如,可以调整或最小化窗口的大小,或者如果不想等待应用程序完成,可以关闭应用程序。

基于异步的方法将自动传输的等效项添加到设计异步作时可以选择的选项列表。 也就是说,你可以获得传统异步编程的所有优势,但开发人员的工作量要少得多。

异步方法易于编写

C# 中的 异步await 关键字是异步编程的核心。 通过使用这两个关键字,可以使用 .NET Framework、.NET Core 或 Windows 运行时中的资源来创建异步方法,就像创建同步方法一样容易。 使用 async 关键字定义的异步方法称为 异步方法

以下示例演示异步方法。 你应对代码中的几乎所有内容都很熟悉。

可以从《使用 C# 中的 async 和 await 进行异步编程》下载一个完整的 Windows Presentation Foundation (WPF)示例。

public async Task<int> GetUrlContentLengthAsync()
{
    using 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> (有关更多选项,请参阅“返回类型”部分)。 方法名称以 Async 结束。 在方法的正文中, GetStringAsync 返回一个 Task<string>。 这意味着在 await 任务时,将获得 string (contents)。 在等待任务之前,可以通过 string 执行不依赖于 GetStringAsync 的工作。

请密切关注 await 运算符。 它会暂停 GetUrlContentLengthAsync

  • GetUrlContentLengthAsync 无法继续,直至 getStringTask 完成。
  • 同时,控件返回至 GetUrlContentLengthAsync 的调用方。
  • getStringTask 完成时,控件将在此处继续。
  • 然后,运算符awaitstring中检索getStringTask结果。

return 语句指定整数结果。 任何等待 GetUrlContentLengthAsync 的方法都会检索长度值。

如果 GetUrlContentLengthAsync 调用 GetStringAsync 和等待其完成之间没有任何工作,可以通过在以下单个语句中调用和等待来简化代码。

string contents = await client.GetStringAsync("https://learn.microsoft.com/dotnet");

以下特征总结了使上一个示例成为异步方法的内容:

  • 方法签名包含async修饰符。

  • 按照约定,异步方法的名称以“Async”后缀结尾。

  • 返回类型是以下类型之一:

    • Task<TResult> 如果方法中有一个返回语句,其中操作数的类型是 TResult
    • Task 如果你的方法没有返回语句,或者只有没有操作数的返回语句。
    • void 如果您正在编写异步事件处理程序。
    • 具有 GetAwaiter 方法的任何其他类型。

    有关详细信息,请参阅 “返回类型和参数 ”部分。

  • 该方法通常包含至少一个 await 表达式,该表达式标记方法在等待的异步操作完成之前无法继续的点。 同时,将方法挂起,并且控件返回到方法的调用方。 本主题的下一节将解释悬挂点发生的情况。

在异步方法中,可使用提供的关键字和类型来指示需要完成的操作,且编译器会完成其余操作,其中包括持续跟踪控件以挂起方法返回等待点时发生的情况。 某些例程进程(如循环和异常处理)在传统异步代码中可能难以处理。 在异步方法中,可以像在同步解决方案中那样编写这些元素,并解决问题。

有关早期版本的 .NET Framework 中的异步的详细信息,请参阅 TPL 和传统的 .NET Framework 异步编程

异步方法中发生的情况

异步编程中要了解的最重要事项是控制流如何从方法移动到方法。 下图将引导你完成此过程:

异步控制流的跟踪导航

关系图中的数字对应于以下步骤,在调用方法调用异步方法时启动。

  1. 调用的方法会调用并等待 GetUrlContentLengthAsync 异步方法。

  2. GetUrlContentLengthAsync 创建一个 HttpClient 实例并调用 GetStringAsync 异步方法以字符串的形式下载网站的内容。

  3. GetStringAsync 中发生的某件事使其进度暂停。 可能必须等待网站下载或一些其他阻止活动。 为了避免阻止资源,GetStringAsync 将控制权交给调用方 GetUrlContentLengthAsync

    GetStringAsync 返回一个 Task<TResult>,其中 TResult 是一个字符串,并将 GetUrlContentLengthAsync 任务 getStringTask 分配给变量。 该任务表示调用 GetStringAsync的正在进行的过程,承诺在完成工作时生成实际字符串值。

  4. 由于尚未等待 getStringTask,因此,GetUrlContentLengthAsync 可以继续执行不依赖于 GetStringAsync 得出的最终结果的其他工作。 该工作由对同步方法 DoIndependentWork的调用表示。

  5. DoIndependentWork 是一种同步方法,用于执行其工作并返回到其调用方。

  6. GetUrlContentLengthAsync 已运行完毕,可以不受 getStringTask 的结果影响。 GetUrlContentLengthAsync next 希望计算并返回下载的字符串的长度,但该方法在方法包含字符串之前无法计算该值。

    因此, GetUrlContentLengthAsync 使用 await 运算符暂停其进度,并将控制权传递给调用 GetUrlContentLengthAsync的方法。 GetUrlContentLengthAsync 返回 Task<int> 给调用方。 该任务承诺生成一个整数结果,该结果表示下载的字符串的长度。

    注释

    如果 GetStringAsync(因此 getStringTask)在 GetUrlContentLengthAsync 等待前完成,则控制会保留在 GetUrlContentLengthAsync 中。 如果调用的异步进程GetUrlContentLengthAsync已完成,并且getStringTask不必等待最终结果,则暂停和返回GetUrlContentLengthAsync的费用将浪费。

    在调用方法中,处理模式会继续进行。 调用方可能会在等待该结果之前执行其他不依赖于结果 GetUrlContentLengthAsync 的工作,或者调用方可能会立即等待。 调用方法正在等待 GetUrlContentLengthAsync,并且 GetUrlContentLengthAsync 正在等待 GetStringAsync

  7. GetStringAsync 完成并生成字符串结果。 调用 GetStringAsync 不会以预期的方式返回字符串结果。 (请记住,该方法已在步骤 3 中返回任务。相反,字符串结果存储在表示方法 getStringTask完成的任务中。 await 运算符从 getStringTask中检索结果。 赋值语句将检索的结果分配给contents

  8. 如果 GetUrlContentLengthAsync 字符串结果为字符串,该方法可以计算字符串的长度。 然后,GetUrlContentLengthAsync 的工作也已完成,等待的事件处理程序可以继续运行。 在主题末尾的完整示例中,可以确认事件处理程序检索并打印长度结果的值。 如果你不熟悉异步编程,请花一分钟时间考虑同步和异步行为之间的差异。 同步方法在工作完成时返回(步骤 5),但异步方法在暂停工作时返回任务值(步骤 3 和步骤 6)。 当异步方法最终完成其工作时,任务将标记为已完成,结果(如果有)存储在任务中。

API 异步方法

你可能想知道在哪里可以找到支持异步编程的方法,例如GetStringAsync 。 .NET Framework 4.5 或更高版本和 .NET Core 包含许多与 asyncawait 配合使用的成员。 可以通过成员名称末尾的“Async”后缀以及它们的返回类型 TaskTask<TResult> 来识别它们。 该 System.IO.Stream 类包含方法,如 CopyToAsyncReadAsyncWriteAsync,以及同步方法 CopyToReadWrite

Windows 运行时还包含许多可与 Windows 应用中的 asyncawait 一同使用的方法。 有关详细信息,请参阅适用于 UWP 开发的线程和异步编程以及异步编程(Windows 应用商店应用)和快速入门:如果使用早期版本的 Windows 运行时,在 C# 或 Visual Basic 中调用异步 API

线程

异步方法旨在进行非阻塞操作。 await异步方法中的表达式不会在等待的任务运行时阻止当前线程。 相反,表达式将该方法的其余部分注册为延续,并将控制权返回到异步方法的调用方。

asyncawait关键字不会导致创建其他线程。 异步方法不需要多线程处理,因为异步方法不会在其自己的线程上运行。 该方法在当前同步上下文上运行,并且仅在方法处于活动状态时在线程上使用时间。 使用 Task.Run 可将 CPU 密集型任务转移到后台线程,然而,对于仅在等待结果可用的进程,后台线程并无帮助。

对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法优于用于 I/O 绑定操作的类,因为代码更简单,且无需防范竞争条件。 与Task.Run方法结合使用时,异步编程在处理CPU密集型任务方面比BackgroundWorker更有优势,因为异步编程将代码运行的协调细节与传输到线程池的任务Task.Run分开。

async 和 await

如果使用 异步修饰符 指定方法是异步方法,则启用以下两项功能。

  • 标记的异步方法可以使用 await 来指定暂停点。 运算符 await 告诉编译器,在等待的异步进程完成之前,异步方法无法继续超过该点。 同时,控件将返回到异步方法的调用方。

    异步方法在 await 表达式执行时暂停并不构成方法退出,只会导致 finally 代码块不运行。

  • 标记的异步方法本身可以通过调用它的方法等待。

异步方法通常包含运算符 await 的一个或多个匹配项,但缺少 await 表达式不会导致编译器错误。 如果异步方法不使用 await 运算符标记挂起点,那么无论是否存在 async 修饰符,该方法都将像同步方法一样执行。 编译器针对此类方法发出警告。

asyncawait 是上下文关键字。 有关详细信息和示例,请参阅以下主题:

返回类型和参数

异步方法通常返回一个 TaskTask<TResult>。 在异步方法内部,await 运算符被应用于从另一个异步方法调用中返回的任务。

当方法包含指定类型为Task<TResult>的操作数的return语句时,应将TResult指定为返回类型。

如果方法没有 return 语句或者返回语句不返回操作数,则使用 Task 作为返回类型。

还可以指定任何其他返回类型,前提是该类型包含方法 GetAwaiterValueTask<TResult> 是此类类型的一个示例。 它在 System.Threading.Tasks.Extension NuGet 包中可用。

以下示例演示如何声明和调用返回Task<TResult>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();

每个返回的任务表示正在进行的工作。 任务可封装有关异步进程状态的信息,如果未成功,则最后会封装来自进程的最终结果或进程引发的异常。

异步方法也可以具有 void 返回类型。 此返回类型主要用于定义事件处理程序,其中返回类型需要是 void。 异步事件处理程序通常充当异步程序的起点。

无法等待具有 void 返回类型的异步方法,并且无效返回方法的调用方捕获不到异步方法引发的任何异常。

异步方法不能声明 inrefout 参数,但该方法可以调用具有此类参数的方法。 同样,异步方法不能通过引用返回值,尽管它可以调用具有 ref 返回值的方法。

有关详细信息和示例,请参阅异步返回类型(C#)。

Windows 运行时编程中的异步 API 具有以下返回类型之一,类似于任务:

命名约定

按照惯例,返回常见可等待类型的方法(例如,TaskTask<T>ValueTaskValueTask<T>)应具备以“Async”结尾的名称。 启动异步操作但不返回可等待类型的方法,其名称不应以“Async”结尾,而可以以“Begin”、“Start”或其他动词开头,以表明此方法不返回或抛出操作结果。

您可以忽略事件、基类或接口合同建议使用其他名称的惯例。 例如,不应重命名常见的事件处理程序,例如 OnButtonClick

相关文章 (Visual Studio)

标题 DESCRIPTION
如何使用 async 和 await 并行发出多个 Web 请求(C#) 演示如何同时启动多个任务。
异步返回类型 (C#) 说明异步方法可以返回的类型,并解释每种类型是否合适。
使用取消令牌作为一种信号机制来取消任务。 演示如何将以下功能添加到异步解决方案:

- 取消任务列表 (C#)
- 在一段时间后取消任务 (C#)
- 在异步任务完成时对其进行处理 (C#)
使用异步进行文件访问 (C#) 列出并演示使用 async 和 await 访问文件的好处。
基于任务的异步模式(TAP) 描述异步模式,该模式基于 TaskTask<TResult> 类型。
第 9 频道上的异步视频 提供有关异步编程的各种视频的链接。

另请参阅