? 启动程序时,系统会在内存中创建一个新的进程。在进程内部,系统创建了一个称为线程的内核对象,代表真正执行的程序(线程是“执行线程”的简称)。一旦进程建立,系统会在 Main 方法的第一行语句处开始线程的执行。
? 典型异步例子:
示例
? 某次代码运行生成的结果如下所示,Call 1 和 Call 2 占用了大部分时间,绝大部分时间都浪费在等待网站的响应上。
? 如果能发起 2 个 CountCharacter 调用,无需等待结果,就可以显著提升性能。
? 某次运行结果如下,新版程序比旧版快 32%,因为 CountToALargeNumber 的 4 次调用都是在 CountCharactersAsync 方法调用等待网站响应的时候进行的。所有这些工作都是在主线程中完成的,没有创建任何额外的线程。
? 该特性由如下 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 表达式和匿名方法也可以作为异步对象。
? 有关 async 的说明:
? 异步方法的结构包含 3 个不同的区域:
第一个 await 表达式之前的部分。
应该只包含少量无需长时间处理的代码。
await 表达式。
表示将被异步执行的代码。
后续部分。
包括执行环境(所在线程信息、目前作用域内的变量值等)以及所需的其他信息。
? 从第一个 await 表达式之前的代码开始同步执行,直到遇见第一个 await。当 await 的任务完成时,方法继续同步执行。如果还有其他 await,则重复上述步骤。
? 当到达 await 表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为 Task 或 Task<T>,则方法将创建一个 Task 对象,表示需一步完成的任务和后续,然后将该 Task 返回到调用方法。因此会产生 2 个控制流:异步方法和调用方法。
? 异步方法内的代码会完成如下工作:
? 同时地,调用方法中的代码将继续其任务,从异步方法中获取 Task<T> 或 ValueTask<T> 对象。当需要实际值时,就引用其 Result 属性。此时,如果异步方法设置了该属性,调用方法就能获得该值并继续;否则将暂停并等待该属性被设置,然后再继续执行。
? 因此,异步方法中的 return 语句“返回”一个结果或到达末尾时,它并没有真正地返回某个值——它只是退出了。
await 表达式
? await 表达式指定了一个异步执行的任务,由 await 关键字和一个空闲对象(称为任务,可能是 Task 类型,也可能不是)组成。默认情况下,这个任务在当前线程上异步执行。
? 一个空闲对象即是 awaitable 类型的实例。awaitable 类型包含了 GetAwaiter 方法,该方法没有参数,返回一个 awaiter 类型的对象。awaiter 类型包含如下成员:
? 同时,包含以下成员之一:
? 实际上,不需要构建自己的 awaitable,而是使用 Task 类或 ValueTask 类,它们是 awaitable 类型。通过 Task.Run 方法来创建 Task,其签名如下。其中 Func<TReturn> 是一个预定义委托(见第 20 章),不包含任何参数,返回类型为 TReturn。
? 下面的实例展示了具体使用方法。
? 表21.1 列出了Task.Run 方法的 8 个重载,表 21.2 展示了可能用到的 4 个委托类型的签名。
? 4 种委托类型对应的使用方法展示如下:
? System.Threading.Tasks 命名空间中有两个类被设计为取消异步操作,分别为 CancellationToken 和 CancellationTokenSource。
? 第一次运行时,保留注释代码,不会取消任务:
? 第二次运行将注释代码取消,则任务将在 3 s 后停止:
异常处理和 await 表达式
? 注意,尽管 Task 抛出了 Exception,但在 Main 的最后,Task 的状态仍然为 RanToCompletion。原因如下:
? C#6.0 后可以在 catch 和 finally 块中使用 await 表达式。在异常不需要终止应用程序时,可以使用 await 来记录日志或运行其他时间较长的任务。如果新的异步任务也产生了一场,则任何原有的异常信息都将丢失。
? 使用 Wait 方法可以等待 Task 对象完成。
? 使用 Task 类中的静态方法等待一组 Task 对象完成。
? 只取消第一行注释,结果如下。
? 只取消第二行注释,结果如下。
? Task.WaitAll 和 Task.WaitAny 的其他重载方法如表 21.3 所示。
? 使用 Task.WhenAll 和 Task.WhenAny 异步等待多个 Task。
? 将 Task.WhenAll 替换为 Task.WhenAny 后,输出结果如下。
? Task.Delay 方法创建一个 Task 对象,该对象将暂停其在线程中的处理,并在一定时间后完成。
? Delay 方法包含 4 个重载,允许以不同方式来指定时间周期,并允许使用 CancellationToken 对象。