C# 图解教程 第5版 —— 第21章 异步编程

21.1 什么是 异步

​ 启动程序时,系统会在内存中创建一个新的进程。在进程内部,系统创建了一个称为线程的内核对象,代表真正执行的程序(线程是“执行线程”的简称)。一旦进程建立,系统会在 Main 方法的第一行语句处开始线程的执行

  • 进程是构成运行程序的资源集合,包括虚地址空间、文件句柄和运行程序所需的其他东西。
  • 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束。
  • 线程可以派生其他线程,因此任意时刻,一个进程都可能包含不同状态的多个线程,执行程序的不同部分。
  • 同一个进程的多个线程共享进程的资源。
  • 系统为处理器执行所调度的单元是线程,而不是进程。

​ 典型异步例子:

  1. 服务器程序接收来自多个客户端程序的服务请求。
  2. 交互式 GUI 程序,如果用户启动了需要消耗大量时间的操作,那么在程序完成前,用户仍能在屏幕上移动窗口,甚至可以取消操作。

示例

  1. 创建 Stopwatch 类的一个实例并启动,用于测量代码中不同任务的执行时间。
  2. 调用 CountCharacters 方法 2 次,下载某网站的内容,返回网站包含的字符数。
  3. 调用 CountToALargeNumber 方法 4 次,该方法仅执行一个消耗一定时间的任务,并循环指定次数。
  4. 最后,打印两个网站的字符数。
image-20231225215026247 image-20231225215111207

​ 某次代码运行生成的结果如下所示,Call 1 和 Call 2 占用了大部分时间,绝大部分时间都浪费在等待网站的响应上。

image-20231225215431983 image-20231225215511444
图 21.1 程序中不同任务所需时间的时间轴

​ 如果能发起 2 个 CountCharacter 调用,无需等待结果,就可以显著提升性能。

  1. 当 DoRun 调用 CountCharactersAsync 时,CountCharactersAsync 将立即返回,然后才真正开始下载字符。该方法返回 Tast<int> 类型的占位符对象,表示计划进行的工作,这个占位符最终将“返回”一个 int。
  2. 执行两次 CountCharactersAsync 方法后,调用 4 次 CountToALargeNumber,同时 CountCharactersAsync 的两次调用继续它们的工作——基本上是等待。
  3. DoRun 的最后两行 从 CountCharactersAsync 调用返回的 Tasks 中获取结果,如果还没有结果,将阻塞并等待。
image-20231225220923206 image-20231225221000137

​ 某次运行结果如下,新版程序比旧版快 32%,因为 CountToALargeNumber 的 4 次调用都是在 CountCharactersAsync 方法调用等待网站响应的时候进行的。所有这些工作都是在主线程中完成的,没有创建任何额外的线程。

image-20231225221034597 image-20231225221129452
图 21.2 async/await 版本的程序的时间轴

21.2 async/await 特性的结构

​ 该特性由如下 3 个部分组成:

  1. 调用方法。
  2. 异步方法。
  3. await 表达式。
image-20240109111217158
图 21.3 async/await 特性的整体结构

21.3 什么是异步方法

​ 异步的方法在完成其所有工作之前就返回到调用方法,然后在调用方法继续执行的时候完成其工作。其特点如下:

  1. 方法中包含 async 方法修饰符。

  2. 包含一个或多个 await 表达式,表示可以异步完成的任务。

  3. 返回类型必须是以下 3 种或不返回(void)。其中 Task 和 Task<T> 的返回对象表示将在未来完成的工作,调用方法和异步方法可以继续执行。

    • Task

      如果调用方法不需要从异步方法中返回某个值,但需要检查异步方法的状态,就可以返回该类型的对象。如果异步方法中有 return,则不能返回任何对象。

      image-20240109112137845
    • Task<T>

      如果调用方法需要从调用中获取类型 T 的值,异步方法的返回类型就必须是 Task<T>。调用方法将通过读取 Task 的 Result 属性来获取该值。

      image-20240109112253209
    • ValueTask<T>

      与 Task<T> 类似,但用于任务结果可能已经可用的情况。由于是值类型,因此可以放在栈上,无需像 Task<T> 对象那样在堆上分配空间。因此,某些情况下可以提高性能。

  4. 任何任何具有公开可访问的 GetAwaiter 方法的类型。

  5. 异步方法的形参不能为 out 或 ref 参数。

  6. 异步方法的名称应该以 Async 后缀结尾。

  7. Lambda 表达式和匿名方法也可以作为异步对象。

image-20240109111159224
图 21.4 异步方法的结构

​ 有关 async 的说明:

  • 异步方法的方法头中必须包含 async 关键字,且必须位于返回类型之前。
  • async 只起到标识作用,表示方法中包含一个或多个 await 表达式,本身不能创建任何异步操作。
  • async 是上下文关键字,可以在其他区域用作标识符。

21.3.1 异步方法的控制流

​ 异步方法的结构包含 3 个不同的区域:

  1. 第一个 await 表达式之前的部分。

    应该只包含少量无需长时间处理的代码。

  2. await 表达式。

    表示将被异步执行的代码。

  3. 后续部分。

    包括执行环境(所在线程信息、目前作用域内的变量值等)以及所需的其他信息。

image-20240109112849837
图 21.5 异步方法中的代码区域

​ 从第一个 await 表达式之前的代码开始同步执行,直到遇见第一个 await。当 await 的任务完成时,方法继续同步执行。如果还有其他 await,则重复上述步骤。

image-20240109113115258
图 21.6 贯穿一个异步方法的控制流

​ 当到达 await 表达式时,异步方法将控制返回到调用方法。如果方法的返回类型为 Task 或 Task<T>,则方法将创建一个 Task 对象,表示需一步完成的任务和后续,然后将该 Task 返回到调用方法。因此会产生 2 个控制流:异步方法和调用方法。

​ 异步方法内的代码会完成如下工作:

  1. 异步执行 await 表达式的空闲任务。
  2. 当 await 表达式完成时,执行后续部分。后续部分也可能包含其他 await 表达式,也将按照相同的步骤处理。
  3. 遇到 return 语句或到达方法末尾时:
    • 如果方法返回 void,控制流将退出。
    • 如果方法返回 Task,则将设置 Task 的状态属性并退出。
    • 如果方法返回 Task<T> 或 ValueTask<T>,则同时设置 Task 的状态属性和 Result 属性,再退出。

​ 同时地,调用方法中的代码将继续其任务,从异步方法中获取 Task<T> 或 ValueTask<T> 对象。当需要实际值时,就引用其 Result 属性。此时,如果异步方法设置了该属性,调用方法就能获得该值并继续;否则将暂停并等待该属性被设置,然后再继续执行。

​ 因此,异步方法中的 return 语句“返回”一个结果或到达末尾时,它并没有真正地返回某个值——它只是退出了。

await 表达式

​ await 表达式指定了一个异步执行的任务,由 await 关键字和一个空闲对象(称为任务,可能是 Task 类型,也可能不是)组成。默认情况下,这个任务在当前线程上异步执行。

image-20240109114402463

​ 一个空闲对象即是 awaitable 类型的实例。awaitable 类型包含了 GetAwaiter 方法,该方法没有参数,返回一个 awaiter 类型的对象。awaiter 类型包含如下成员:

image-20240109114534127

​ 同时,包含以下成员之一:

image-20240109114600625

​ 实际上,不需要构建自己的 awaitable,而是使用 Task 类或 ValueTask 类,它们是 awaitable 类型。通过 Task.Run 方法来创建 Task,其签名如下。其中 Func<TReturn> 是一个预定义委托(见第 20 章),不包含任何参数,返回类型为 TReturn。

image-20240109114905084

​ 下面的实例展示了具体使用方法。

  • a 使用 Get10 创建名为 ten 的 Func<int> 委托。
  • b 在 Task.Run 方法的参数列表中创建 Func<int> 委托。
  • c 使用 Lambda 表达式并隐式转化为 Func<int> 委托。
image-20240110154447289 image-20240110154521441

​ 表21.1 列出了Task.Run 方法的 8 个重载,表 21.2 展示了可能用到的 4 个委托类型的签名。

表 21.1 Task.Run 重载的返回类型和签名
image-20240110154802718
表 21.2 可作为 Task.Run 方法第一个参数的委托类型
image-20240110154853535

​ 4 种委托类型对应的使用方法展示如下:

image-20240110155946961 image-20240110155959089

21.3.2 取消一个异步操作

​ System.Threading.Tasks 命名空间中有两个类被设计为取消异步操作,分别为 CancellationToken 和 CancellationTokenSource。

  • CancellationToken 包含一个任务是否应被取消的信息 IsCancellationRequested。
  • 拥有 CancellationToken 对象的任务需要定期检查其令牌状态,如果 IsCancellationRequested 属性为 true,任务需停止其操作并返回。
  • CancellationToken 不可逆,且只能使用一次。即,一旦 IsCancellationRequested 被设置为 true,就不能更改。
  • CancellationTokenSource 对象创建可分配给不同任务的 CancellationToken 对象,任何持有 CancellationTokenSource 的对象都可以调用其 Cancel 方法,这将使 IsCancellationRequested 被设置为 true。
image-20240110160458050

​ 第一次运行时,保留注释代码,不会取消任务:

image-20240110160832360

​ 第二次运行将注释代码取消,则任务将在 3 s 后停止:

image-20240110160908305

异常处理和 await 表达式

image-20240110160943773 image-20240110160953537

​ 注意,尽管 Task 抛出了 Exception,但在 Main 的最后,Task 的状态仍然为 RanToCompletion。原因如下:

  1. Task 没有被取消。
  2. 没有未处理的异常。(IsFaulted 为 False 也是因为这个原因)

​ C#6.0 后可以在 catch 和 finally 块中使用 await 表达式。在异常不需要终止应用程序时,可以使用 await 来记录日志或运行其他时间较长的任务。如果新的异步任务也产生了一场,则任何原有的异常信息都将丢失。

21.3.3 在调用方法中同步地等待任务

​ 使用 Wait 方法可以等待 Task 对象完成。

image-20240110162930583 image-20240110163013407

​ 使用 Task 类中的静态方法等待一组 Task 对象完成。

  • Task.WaitAll
  • Task.WaitAny
image-20240110164104353 image-20240110164137902 image-20240110164304798

​ 只取消第一行注释,结果如下。

image-20240110164332814 image-20240110164343091

​ 只取消第二行注释,结果如下。

image-20240110164414643 image-20240110164423894

​ Task.WaitAll 和 Task.WaitAny 的其他重载方法如表 21.3 所示。

表 21.3 Task.WaitAll 和 Task.WaitAny 的重载方法
image-20240110164441174

21.3.4 在异步方法中异步地等待任务

​ 使用 Task.WhenAll 和 Task.WhenAny 异步等待多个 Task。

image-20240110164855240 image-20240110164939247 image-20240110164956414

​ 将 Task.WhenAll 替换为 Task.WhenAny 后,输出结果如下。

image-20240110165030105

21.3.5 Task.Delay 方法

​ Task.Delay 方法创建一个 Task 对象,该对象将暂停其在线程中的处理,并在一定时间后完成。

  • Thread.Sleep:阻塞线程。
  • Task.Delay:不阻塞线程,线程可以继续处理其他工作。
image-20240110165216224 image-20240110165225129

​ Delay 方法包含 4 个重载,允许以不同方式来指定时间周期,并允许使用 CancellationToken 对象。

表 21.4 Task.Delay 方法的重载
image-20240110165457101

21.4 GUI 程序中的异步操作(*)

21.5 使用异步 Lambda 表达式(*)

21.6 一个完整的 GUI 示例

21.7 BackgroundWorker 类(*)

21.8 并行循环(*)

21.9 其他异步编程模式(*)

21.10 BeginInvoke 和 EndInvoke(*)

21.11 计时器(*)

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
《C How to Program,Fifth Edition》(中文名:《C大学教程》),作者:【美】P.J.Deitel、【美】H.M.Deitel,翻译:苏小红、李东、王甜甜,出社:电子工业出社,ISBN:978-7-121-05662-8,PDF 格式,大小 352 Mb,被压缩为 7 部分,本资源是第一部分;第一部分下载地址:http://download.csdn.net/source/3289551;第二部分下载地址:http://download.csdn.net/source/3286123;第三部分下载地址:http://download.csdn.net/source/3285981;第四部分下载地址:http://download.csdn.net/source/3284551;第五部分下载地址:http://download.csdn.net/source/3283848;第六部分下载地址:http://download.csdn.net/source/3290125;第七部分下载地址:http://download.csdn.net/source/3280289。 七个资源在一起解压缩后(windows 平台下解压缩)即可得到《C大学教程(第五·中文)》.pdf 一书。 内容简介: 本书是全球最畅销的C语言教程之一。本书系统地介绍了四种当今流行的程序设计方法——面向过程、基于对象、面向对象以及泛型编程,内容全面、生动、易懂,作者由浅入深地介绍结构化编程及软件工程的基本概念,从简单的概念到最终完整的语言描述,清晰、准确、透彻、详细地讲解C语言,尤其注重程序设计思想和方法的介绍。相对于以往的本,在内容方面,本书新增加了介绍C99标准、排序、基于Allegro C函数库的游戏编程以及有关C++面向对象程序设计的章节,并且在附录中增加了Sudoku游戏程序设计的讲解。新加入的“活代码”方法(即通过可执行的代码来讲解理论概念的方法)是本书的另一特色,它可以促进学生积极地参与到程序设计中来。突出显示的各种程序设计提示,则是作者多年教学经验的总结。 本书不仅适合于初学者学习,作为高校计算机程序设计教学的教科书,也同样适用于有经验的程序员,作为软件开发人员的专业参考书。 内容预览: 第1章 计算机、Internet和万维网导论 第2章 C语言程序设计入门 第3章 结构化的C程序设计 第4章 C程序控制 第5章 C函数 第6章 C数组 第7章 C指针 第8章 C字符和字符串 第9章 格式化输入输出 第10章 结构体、共用体、位操作和枚举类型 第11章 文字处理 第12章 数据结构 第13章 C预处理 第14章 C语言的其他专题 第15章 基于Allegro C函数库的游戏编程 第16章 排序:更深入的透视 第17章 C99简介 第18章 C++,一个更好的C;介绍对象技术 第19章 类与对象简介 第20章 类;深入剖析(第1部分) 第21章 类;深入剖析(第2部分) 第22章 运算符重载 第23章 面向对象编辑:继承 第24章 面向对象编程:多态 第25章 模板 第26章 输入/输出 第27章 异常处理 附录A 因特网和Web资源、 附录B 运算符优先级表、 附录C ASCLL字符集 附录D 数制系统 附录E 游戏编程:求解Sudoku问题 索引

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蔗理苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值