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

任务异步编程模型,简写为TAP
, 全称:Task asynchronous programming model
, 这是从C#5
添加的新特性,TAP
的最大优势是让用户可以以同步模式的代码结构,使用TAP
获得异步编程的优势。在传统的异步编程中,用户需要处理很多和异步相关的知识点,在C#
中这些都由编译器来完成,该特性自从.Net Framework 4.5
之后,以及.Net Core
, Windows Runtime
都支持该特性。
本节是对于TAP
的概述,列出相关的知识点,同时我们在后期的文章里会一一探讨本节列出的知识点。
异步提升响应
对于很多可能会产生阻塞的场景,异步是自然而然的选择,类似访问web服务,特别是在缓慢或者有较大延迟的web服务上,如果在这个场景里使用同步模式,那么整个应用都会被阻塞,导致整个应用不得不等待返回。在异步模式中,应用可以继续其他的任务,而无需等待缓慢web服务的返回。
下述列表展示了经典的异步应用场景,主要包括.Net
以及Windows Runtime
的API列表
应用领域 | 提供异步方法的.Net 类型 | 提供异步方法的Windows Runtime 类型 |
---|---|---|
Web服务存取 | HttpClient | Windows.Web.Http.HttpClient , SyndicationClient |
文件处理 | JsonSerializer , StreamReader , StreamWriter , XmlReader , XmlWriter | StorageFile |
WCF编程 | 同步和异步操作 |
异步编程对于基于UI编程特别适合,因为所有的UI事件都是共享同一个UI线程,如果因为某些操作导致UI线程被阻塞,那么整个应用的UI基本无法响应用户的请求。
易于编写的异步方法
关键字async
和await
是异步编程模型的核心,使用这两个关键字可以很方便的直接使用.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
返回一个字符串。
如果方法GetUrlContentLengthAsync
和getStringTask
之间没有其他的动作需要处理,也可以直接使用:
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
的对象返回,控制点才会恢复,当前方法也会恢复挂起。
在使用async
和await
关键字的时候,用户只需要按照约定使用关键字,其他的事情都由编译器来完成,包括跟踪控制点的转移,跟踪挂起方法的状态,以及await的代码运行以及恢复等等。
如果想了解没有TAP
和async
以及await
方案的时候,异步方法是如何编码的,可以参考我们后面关于TPL
的系列文章。
详细的解析异步方法中到底发生了什么
当我们调用一个异步方法的时候,到底发生了什么?我们可以使用下面的图来详细的解释,当一个异步方法被调用的时候,到底发生了什么。
按照这张图例的步骤号我们来详细的解析一下异步方法的整个过程。
- 一个方法开始调用方法
GetUrlContentLengthAsync
, 并await
它的结果 GetUrlContentLengthAsync
方法创建了一个HttpClient
客户端,并且调用HttpClient
的方法GetStringAsync
异步方法来从一个web服务器获得内容。- 在取得web服务器的时候,可能由于某些原因发生了延迟,导致方法
GetStringAsync
必须等待web服务器返回的结果,为了避免被阻塞,GetStringAsync
让出控制权给方法GetUrlContentLengthAsync
, 同时GetStringAsync
返回一个Task<string>
对象,这个对象代表还在运行的一个任务,(这里也说明异步方法的运行是在调用的时候就已经开始了) - 由于
getStringTask
还没有被await
, 而且由于控制权也已经返回给GetUrlContentLengthAsync
, 因此它会继续运行另外一个方法DoIndependentWork()
。 - 方法
DoIndependentWork
是一个同步方法,程序控制点进入到该方法之后,开始运行,并且阻塞了主方法。 GetUrlContentLengthAsync
方法已经没有其他需要运行的方法了,因此开始在这里await
getStringTask
, 由于调用了await
, 对于方法体来说,它不得不挂起等待await
的结果,同时这里需要注意由于await, 程序的控制点返回给了调用方法GetUrlContentLengthAsync
的调用者。- 等候await的返回结果。
- 挂起恢复,并返回结果给调用者。
在这整个步骤的解析中,需要注意这些事实:
- 异步方法在调用的时候就已经运行了,不是非要等到await才开始运行
- 异步方法在调用的时候,就已经将控制权还给了方法体,不是要等到await,
- await是挂起自己的方法体,并将控制权返回给更上一层的调用者,不是返回控制权到自己的方法体。
- 一定要注意控制点的转换
API with Async support
在.Net
或者Windows runtime
中基本都遵行命名的约束,也就是异步方法都是以Async
结尾。可以参考API的手册判断该方法是否支持异步。
异步方法和线程
异步方法的设计目的是不会被阻塞当前的执行线程,异步方法并不会使得CLR
创建更多的线程,而且也要求必须支持多线程的情况,另外需要注意的是async
方法并不是在当前的执行线程中运行。针对于基于CPU的异步场景,我们前面也讲过了,需要使用Task.Run()
将任务放入到后台线程池,由后台线程池来完成这个工作,这些工作由编译器和CLR
完成。
async
和await
关键字
当使用关键字async
修饰一个方法的时候,说明:
- 这个方法被标记成了一个异步方法,同时允许你在方法体中使用
await
定义一个挂起的点,调用方法遇到await
之后,程序控制点会该位置返回,方法体被挂起。 - 也表明方法本身可以被
await
返回类型和参数
我们前面也讨论过了,如果异步方法有返回值,则使用Task<T>
来表示返回的值,如果是没有返回值,则Task
就可以了。
另外需要注意的是在异步方法中,不能使用in
, ref
, out
等参数。
命名约定
基本的命名约定是异步方法最好以Async
结尾。
本节最为重要的就是要理解控制点,异步方法的运行等等基本的原理,后面我们会更深入的学习相关的概念。