文章目录
21.1 什么是 异步
启动程序时,系统会在内存中创建一个新的进程。在进程内部,系统创建了一个称为线程的内核对象,代表真正执行的程序(线程是“执行线程”的简称)。一旦进程建立,系统会在 Main 方法的第一行语句处开始线程的执行。
- 进程是构成运行程序的资源集合,包括虚地址空间、文件句柄和运行程序所需的其他东西。
- 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。
- 线程可以派生其他线程,因此任意时刻,一个进程都可能包含不同状态的多个线程,执行程序的不同部分。
- 同一个进程的多个线程共享进程的资源。
- 系统为处理器执行所调度的单元是线程,而不是进程。
典型异步例子:
- 服务器程序接收来自多个客户端程序的服务请求。
- 交互式 GUI 程序,如果用户启动了需要消耗大量时间的操作,那么在程序完成前,用户仍能在屏幕上移动窗口,甚至可以取消操作。
示例
- 创建 Stopwatch 类的一个实例并启动,用于测量代码中不同任务的执行时间。
- 调用 CountCharacters 方法 2 次,下载某网站的内容,返回网站包含的字符数。
- 调用 CountToALargeNumber 方法 4 次,该方法仅执行一个消耗一定时间的任务,并循环指定次数。
- 最后,打印两个网站的字符数。
![image-20231225215026247](https://i-blog.csdnimg.cn/blog_migrate/65b828e62ac3ac84273d4a69a8ff795a.png)
![image-20231225215111207](https://i-blog.csdnimg.cn/blog_migrate/4c7ed45240201591d4a8abdf9b43d188.png)
某次代码运行生成的结果如下所示,Call 1 和 Call 2 占用了大部分时间,绝大部分时间都浪费在等待网站的响应上。
![image-20231225215431983](https://i-blog.csdnimg.cn/blog_migrate/c5047af06ce02a7091ba182b86e10ba8.png)
![image-20231225215511444](https://i-blog.csdnimg.cn/blog_migrate/59da2cd3a7414f6869ae8ed55576d9f6.png)
如果能发起 2 个 CountCharacter 调用,无需等待结果,就可以显著提升性能。
- 当 DoRun 调用 CountCharactersAsync 时,CountCharactersAsync 将立即返回,然后才真正开始下载字符。该方法返回 Tast<int> 类型的占位符对象,表示计划进行的工作,这个占位符最终将“返回”一个 int。
- 执行两次 CountCharactersAsync 方法后,调用 4 次 CountToALargeNumber,同时 CountCharactersAsync 的两次调用继续它们的工作——基本上是等待。
- DoRun 的最后两行 从 CountCharactersAsync 调用返回的 Tasks 中获取结果,如果还没有结果,将阻塞并等待。
![image-20231225220923206](https://i-blog.csdnimg.cn/blog_migrate/7394cf0cd077c54c99105faf2f9b8b53.png)
![image-20231225221000137](https://i-blog.csdnimg.cn/blog_migrate/10bac54b57209510986d84d16df96815.png)
某次运行结果如下,新版程序比旧版快 32%,因为 CountToALargeNumber 的 4 次调用都是在 CountCharactersAsync 方法调用等待网站响应的时候进行的。所有这些工作都是在主线程中完成的,没有创建任何额外的线程。
![image-20231225221034597](https://i-blog.csdnimg.cn/blog_migrate/d241c50befcf37a4024ceea85c1a16e7.png)
![image-20231225221129452](https://i-blog.csdnimg.cn/blog_migrate/d4688fec7e83947f0f0829db16f37f58.png)
21.2 async/await 特性的结构
该特性由如下 3 个部分组成:
- 调用方法。
- 异步方法。
- await 表达式。
![image-20240109111217158](https://i-blog.csdnimg.cn/blog_migrate/67860a57dc82f239f7b78a4e19759b9d.png)
21.3 什么是异步方法
异步的方法在完成其所有工作之前就返回到调用方法,然后在调用方法继续执行的时候完成其工作。其特点如下:
-
方法中包含 async 方法修饰符。
-
包含一个或多个 await 表达式,表示可以异步完成的任务。
-
返回类型必须是以下 3 种或不返回(void)。其中 Task 和 Task<T> 的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。
-
Task
如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,就可以返回该类型的对象。如果异步方法中有 return,则不能返回任何对象。
-
Task<T>
如果调用方法需要从调用中获取类型 T 的值,异步方法的返回类型就必须是 Task<T>。调用方法将通过读取 Task 的 Result 属性来获取该值。
-
ValueTask<T>
与 Task<T> 类似,但用于任务结果可能已经可用的情况。由于是值类型,因此可以放在栈上,无需像 Task<T> 对象那样在堆上分配空间。因此,某些情况下可以提高性能。
-
-
任何任何具有公开可访问的 GetAwaiter 方法的类型。
-
异步方法的形参不能为 out 或 ref 参数。
-
异步方法的名称应该以 Async 后缀结尾。
-
Lambda 表达式和匿名方法也可以作为异步对象。
![image-20240109111159224](https://i-blog.csdnimg.cn/blog_migrate/48cdd44a2d7b21ad2d8bb6bc1346cd37.png)
有关 async 的说明:
- 异步方法的方法头中必须包含 async 关键字,且必须位于返回类型之前。
- async 只起到标识作用,表示方法中包含一个或多个 await 表达式,本身不能创建任何异步操作。
- async 是上下文关键字,可以在其他区域用作标识符。
21.3.1 异步方法的控制流
异步方法的结构包含 3 个不同的区域:
-
第一个 await 表达式之前的部分。
应该只包含少量无需长时间处理的代码。
-
await 表达式。
表示将被异步执行的代码。
-
后续部分。
包括执行环境(所在线程信息、目前作用域内的变量值等)以及所需的其他信息。
![image-20240109112849837](https://i-blog.csdnimg.cn/blog_migrate/aee11d740284ce6b1ecb0762ef0a6bad.png)
从第一个 await 表达式之前的代码开始同步执行,直到遇见第一个 await。当 await 的任务完成时,方法继续同步执行。如果还有其他 await,则重复上述步骤。
![image-20240109113115258](https://i-blog.csdnimg.cn/blog_migrate/73dd11ed6627cf04083df35330e529ce.png)
当到达 await 表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为 Task 或 Task<T>,则方法将创建一个 Task 对象,表示需一步完成的任务和后续,然后将该 Task 返回到调用方法。因此会产生 2 个控制流:异步方法和调用方法。
异步方法内的代码会完成如下工作:
- 异步执行 await 表达式的空闲任务。
- 当 await 表达式完成时,执行后续部分。后续部分也可能包含其他 await 表达式,也将按照相同的步骤处理。
- 遇到 return 语句或到达方法末尾时:
- 如果方法返回 void,控制流将退出。
- 如果方法返回 Task,则将设置 Task 的状态属性并退出。
- 如果方法返回 Task<T> 或 ValueTask<T>,则同时设置 Task 的状态属性和 Result 属性,再退出。
同时地,调用方法中的代码将继续其任务,从异步方法中获取 Task<T> 或 ValueTask<T> 对象。当需要实际值时,就引用其 Result 属性。此时,如果异步方法设置了该属性,调用方法就能获得该值并继续;否则将暂停并等待该属性被设置,然后再继续执行。
因此,异步方法中的 return 语句“返回”一个结果或到达末尾时,它并没有真正地返回某个值——它只是退出了。
await 表达式
await 表达式指定了一个异步执行的任务,由 await 关键字和一个空闲对象(称为任务,可能是 Task 类型,也可能不是)组成。默认情况下,这个任务在当前线程上异步执行。
![image-20240109114402463](https://i-blog.csdnimg.cn/blog_migrate/77a76c76991eb7ed8b3752af3e76fcf7.png)
一个空闲对象即是 awaitable 类型的实例。awaitable 类型包含了 GetAwaiter 方法,该方法没有参数,返回一个 awaiter 类型的对象。awaiter 类型包含如下成员:
![image-20240109114534127](https://i-blog.csdnimg.cn/blog_migrate/95876840b10483443ab89433ad86af73.png)
同时,包含以下成员之一:
![image-20240109114600625](https://i-blog.csdnimg.cn/blog_migrate/deea0ad8c3592fa95d54c3705ec12971.png)
实际上,不需要构建自己的 awaitable,而是使用 Task 类或 ValueTask 类,它们是 awaitable 类型。通过 Task.Run 方法来创建 Task,其签名如下。其中 Func<TReturn> 是一个预定义委托(见第 20 章),不包含任何参数,返回类型为 TReturn。
![image-20240109114905084](https://i-blog.csdnimg.cn/blog_migrate/46ec3813d0ecec018f1376ae894f0fb7.png)
下面的实例展示了具体使用方法。
- a 使用 Get10 创建名为 ten 的 Func<int> 委托。
- b 在 Task.Run 方法的参数列表中创建 Func<int> 委托。
- c 使用 Lambda 表达式并隐式转化为 Func<int> 委托。
![image-20240110154447289](https://i-blog.csdnimg.cn/blog_migrate/216a3b0ed0b5c987c6aa685320ba4f71.png)
![image-20240110154521441](https://i-blog.csdnimg.cn/blog_migrate/92168a08a78af460d3a7d46fec913eb5.png)
表21.1 列出了Task.Run 方法的 8 个重载,表 21.2 展示了可能用到的 4 个委托类型的签名。
![image-20240110154802718](https://i-blog.csdnimg.cn/blog_migrate/2340cba9ad316f283fe6dd36d91e2a13.png)
![image-20240110154853535](https://i-blog.csdnimg.cn/blog_migrate/25e01d2c57ff02d61835ca16f63d1fb3.png)
4 种委托类型对应的使用方法展示如下:
![image-20240110155946961](https://i-blog.csdnimg.cn/blog_migrate/e6e1d44329859498964108df1e14a6c9.png)
![image-20240110155959089](https://i-blog.csdnimg.cn/blog_migrate/c7484784cf892103af34ae804a648c88.png)
21.3.2 取消一个异步操作
System.Threading.Tasks 命名空间中有两个类被设计为取消异步操作,分别为 CancellationToken 和 CancellationTokenSource。
- CancellationToken 包含一个任务是否应被取消的信息 IsCancellationRequested。
- 拥有 CancellationToken 对象的任务需要定期检查其令牌状态,如果 IsCancellationRequested 属性为 true,任务需停止其操作并返回。
- CancellationToken 不可逆,且只能使用一次。即,一旦 IsCancellationRequested 被设置为 true,就不能更改。
- CancellationTokenSource 对象创建可分配给不同任务的 CancellationToken 对象,任何持有 CancellationTokenSource 的对象都可以调用其 Cancel 方法,这将使 IsCancellationRequested 被设置为 true。
![image-20240110160458050](https://i-blog.csdnimg.cn/blog_migrate/cc0d68ac94a55cfc69e9a1faaf9b6320.png)
第一次运行时,保留注释代码,不会取消任务:
![image-20240110160832360](https://i-blog.csdnimg.cn/blog_migrate/11e264077da741d62f50e4c3d9ff6ade.png)
第二次运行将注释代码取消,则任务将在 3 s 后停止:
![image-20240110160908305](https://i-blog.csdnimg.cn/blog_migrate/444ddac5d6b57d04e66fe05d7d3f2d61.png)
异常处理和 await 表达式
![image-20240110160943773](https://i-blog.csdnimg.cn/blog_migrate/425dd18906bf3370ff2293f00c8fc926.png)
![image-20240110160953537](https://i-blog.csdnimg.cn/blog_migrate/75004970613390b754d6cde853d8c416.png)
注意,尽管 Task 抛出了 Exception,但在 Main 的最后,Task 的状态仍然为 RanToCompletion。原因如下:
- Task 没有被取消。
- 没有未处理的异常。(IsFaulted 为 False 也是因为这个原因)
C#6.0 后可以在 catch 和 finally 块中使用 await 表达式。在异常不需要终止应用程序时,可以使用 await 来记录日志或运行其他时间较长的任务。如果新的异步任务也产生了一场,则任何原有的异常信息都将丢失。
21.3.3 在调用方法中同步地等待任务
使用 Wait 方法可以等待 Task 对象完成。
![image-20240110162930583](https://i-blog.csdnimg.cn/blog_migrate/fc6a92a25458f975f01f5dbebb7ec34c.png)
![image-20240110163013407](https://i-blog.csdnimg.cn/blog_migrate/eb7ed76da277a74ba25f49efc8d0e799.png)
使用 Task 类中的静态方法等待一组 Task 对象完成。
- Task.WaitAll
- Task.WaitAny
![image-20240110164104353](https://i-blog.csdnimg.cn/blog_migrate/bc34bf80f4b7892a76d9c5549df3028c.png)
![image-20240110164137902](https://i-blog.csdnimg.cn/blog_migrate/9cf27362166c2948657090f25b2c4c9e.png)
![image-20240110164304798](https://i-blog.csdnimg.cn/blog_migrate/847d8522cf1512a2d97972440fd2d0f0.png)
只取消第一行注释,结果如下。
![image-20240110164332814](https://i-blog.csdnimg.cn/blog_migrate/f88b23de286d53fc1529b51f5f6f82a9.png)
![image-20240110164343091](https://i-blog.csdnimg.cn/blog_migrate/2b72784b6769a1b7dd848b06009934cb.png)
只取消第二行注释,结果如下。
![image-20240110164414643](https://i-blog.csdnimg.cn/blog_migrate/1b3bc729c00f131bc40b47b807e15078.png)
![image-20240110164423894](https://i-blog.csdnimg.cn/blog_migrate/11a3c4eaf05d7476f57ccf6987153113.png)
Task.WaitAll 和 Task.WaitAny 的其他重载方法如表 21.3 所示。
![image-20240110164441174](https://i-blog.csdnimg.cn/blog_migrate/5134502f9deca55b09c25ae894e5059e.png)
21.3.4 在异步方法中异步地等待任务
使用 Task.WhenAll 和 Task.WhenAny 异步等待多个 Task。
![image-20240110164855240](https://i-blog.csdnimg.cn/blog_migrate/fdf807bac38800c76086ad8baecabc5c.png)
![image-20240110164939247](https://i-blog.csdnimg.cn/blog_migrate/8a5e51ad888a1259d6c0ffa3e8af0def.png)
![image-20240110164956414](https://i-blog.csdnimg.cn/blog_migrate/ef4e02c2c9fd6b6129d323ca75c32ee3.png)
将 Task.WhenAll 替换为 Task.WhenAny 后,输出结果如下。
![image-20240110165030105](https://i-blog.csdnimg.cn/blog_migrate/76c47466dbbe77890d93f7f49adb4f65.png)
21.3.5 Task.Delay 方法
Task.Delay 方法创建一个 Task 对象,该对象将暂停其在线程中的处理,并在一定时间后完成。
- Thread.Sleep:阻塞线程。
- Task.Delay:不阻塞线程,线程可以继续处理其他工作。
![image-20240110165216224](https://i-blog.csdnimg.cn/blog_migrate/401afae31725c8bb465772d715538492.png)
![image-20240110165225129](https://i-blog.csdnimg.cn/blog_migrate/87b3d3910f45b672a5d129507e9c2344.png)
Delay 方法包含 4 个重载,允许以不同方式来指定时间周期,并允许使用 CancellationToken 对象。
![image-20240110165457101](https://i-blog.csdnimg.cn/blog_migrate/d6d92b254a2fe674920d5ce1c6eea714.png)