Skip to main content

任务异步编程模型

分类:  .Net技术 标签:  #异步编程 #基础 #.Net 发布于: 2023-06-04 19:04:57

任务异步编程模型,简写为TAP, 全称:Task asynchronous programming model, 这是从C#5添加的新特性,TAP的最大优势是让用户可以以同步模式的代码结构,使用TAP获得异步编程的优势。在传统的异步编程中,用户需要处理很多和异步相关的知识点,在C#中这些都由编译器来完成,该特性自从.Net Framework 4.5之后,以及.Net CoreWindows Runtime都支持该特性。

本节是对于TAP的概述,列出相关的知识点,同时我们在后期的文章里会一一探讨本节列出的知识点。

异步提升响应

对于很多可能会产生阻塞的场景,异步是自然而然的选择,类似访问web服务,特别是在缓慢或者有较大延迟的web服务上,如果在这个场景里使用同步模式,那么整个应用都会被阻塞,导致整个应用不得不等待返回。在异步模式中,应用可以继续其他的任务,而无需等待缓慢web服务的返回。

下述列表展示了经典的异步应用场景,主要包括.Net以及Windows Runtime的API列表

应用领域提供异步方法的.Net类型提供异步方法的Windows Runtime类型
Web服务存取HttpClientWindows.Web.Http.HttpClientSyndicationClient
文件处理JsonSerializerStreamReaderStreamWriterXmlReaderXmlWriterStorageFile
WCF编程同步和异步操作 

异步编程对于基于UI编程特别适合,因为所有的UI事件都是共享同一个UI线程,如果因为某些操作导致UI线程被阻塞,那么整个应用的UI基本无法响应用户的请求。

易于编写的异步方法

关键字asyncawait是异步编程模型的核心,使用这两个关键字可以很方便的直接使用.Net framework.Net core以及Windows runtime中的资源,代码的结构几乎和在写同步代码时没有太大的区别,定义异步方法只需要在方法定义时直接使用关键字async定义该方法。

你可以从如下的URL中找到关于异步方法的实例:https://docs.microsoft.com/en-us/samples/dotnet/samples/async-and-await-cs, 请参考如下的实例代码:

public async Task<int> GetUrlContentLengthAsync()
{
    var client = new HttpClient();

    Task<string> getStringTask =
        client.GetStringAsync("https://docs.microsoft.com/dotnet");

    DoIndependentWork();

    string contents = await getStringTask;

    return contents.Length;
}

void DoIndependentWork()
{
    Console.WriteLine("Working...");
}

从上述实例可以看到一个异步方法有如下的元素:

  • 方法定义中使用了aysnc关键字。
  • 方法的返回值是Task或者是Task<T>
  • 方法体中最少有一个await的操作。

对应的这个实例可以看到:

  • GetUrlContentLengthAsync一直要等到方法getStringTask完成才会继续。
  • 控制点会返回到方法GetUrlContentLengthAsync的调用者。
  • getStringTask完成控制点才会恢复。
  • await操作从getStringTask返回一个字符串。

如果方法GetUrlContentLengthAsyncgetStringTask之间没有其他的动作需要处理,也可以直接使用:

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

从上面的实例可以总结一个异步方法的要点:

  • 方法的签名必须是使用async
  • 方法命名的结尾按照约定使用Async
  • 方法的返回值有如下几种情况:
    • 如果方法需要返回某些值,使用Task<T>的泛型版本,await操作会帮助返回值。
    • 如果方法无需返回值,使用Task
    • 在事件event中使用void, 而不是Task
    • C# 7.0开始可以返回任何支持方法GetAwaiter的类型
  • 通常方法体中至少包含一个await, 当代码运行到await时,程序的控制点会返回到调用者,当前方法被挂起,直到await的对象返回,控制点才会恢复,当前方法也会恢复挂起。

在使用asyncawait关键字的时候,用户只需要按照约定使用关键字,其他的事情都由编译器来完成,包括跟踪控制点的转移,跟踪挂起方法的状态,以及await的代码运行以及恢复等等。

如果想了解没有TAPasync以及await方案的时候,异步方法是如何编码的,可以参考我们后面关于TPL的系列文章。

详细的解析异步方法中到底发生了什么

当我们调用一个异步方法的时候,到底发生了什么?我们可以使用下面的图来详细的解释,当一个异步方法被调用的时候,到底发生了什么。


按照这张图例的步骤号我们来详细的解析一下异步方法的整个过程。

  1. 一个方法开始调用方法GetUrlContentLengthAsync, 并await它的结果
  2. GetUrlContentLengthAsync方法创建了一个HttpClient客户端,并且调用HttpClient的方法GetStringAsync异步方法来从一个web服务器获得内容。
  3. 在取得web服务器的时候,可能由于某些原因发生了延迟,导致方法GetStringAsync必须等待web服务器返回的结果,为了避免被阻塞,GetStringAsync让出控制权给方法GetUrlContentLengthAsync, 同时GetStringAsync返回一个Task<string>对象,这个对象代表还在运行的一个任务,(这里也说明异步方法的运行是在调用的时候就已经开始了)
  4. 由于getStringTask还没有被await, 而且由于控制权也已经返回给GetUrlContentLengthAsync, 因此它会继续运行另外一个方法DoIndependentWork()
  5. 方法DoIndependentWork是一个同步方法,程序控制点进入到该方法之后,开始运行,并且阻塞了主方法。
  6. GetUrlContentLengthAsync方法已经没有其他需要运行的方法了,因此开始在这里await getStringTask, 由于调用了await, 对于方法体来说,它不得不挂起等待await的结果,同时这里需要注意由于await, 程序的控制点返回给了调用方法GetUrlContentLengthAsync的调用者。
  7. 等候await的返回结果。
  8. 挂起恢复,并返回结果给调用者。

在这整个步骤的解析中,需要注意这些事实:

  • 异步方法在调用的时候就已经运行了,不是非要等到await才开始运行
  • 异步方法在调用的时候,就已经将控制权还给了方法体,不是要等到await,
  • await是挂起自己的方法体,并将控制权返回给更上一层的调用者,不是返回控制权到自己的方法体。
  • 一定要注意控制点的转换

API with Async support

.Net或者Windows runtime中基本都遵行命名的约束,也就是异步方法都是以Async结尾。可以参考API的手册判断该方法是否支持异步。

异步方法和线程

异步方法的设计目的是不会被阻塞当前的执行线程,异步方法并不会使得CLR创建更多的线程,而且也要求必须支持多线程的情况,另外需要注意的是async方法并不是在当前的执行线程中运行。针对于基于CPU的异步场景,我们前面也讲过了,需要使用Task.Run()将任务放入到后台线程池,由后台线程池来完成这个工作,这些工作由编译器和CLR完成。

asyncawait关键字

当使用关键字async修饰一个方法的时候,说明:

  • 这个方法被标记成了一个异步方法,同时允许你在方法体中使用await 定义一个挂起的点,调用方法遇到await之后,程序控制点会该位置返回,方法体被挂起。
  • 也表明方法本身可以被await

返回类型和参数

我们前面也讨论过了,如果异步方法有返回值,则使用Task<T>来表示返回的值,如果是没有返回值,则Task就可以了。
另外需要注意的是在异步方法中,不能使用inrefout等参数。

命名约定

基本的命名约定是异步方法最好以Async结尾。

本节最为重要的就是要理解控制点,异步方法的运行等等基本的原理,后面我们会更深入的学习相关的概念。