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

文章目录

    • 21.1 什么是 异步
    • 21.2 async/await 特性的结构
    • 21.3 什么是异步方法
      • 21.3.1 异步方法的控制流
      • 21.3.2 取消一个异步操作
      • 21.3.3 在调用方法中同步地等待任务
      • 21.3.4 在异步方法中异步地等待任务
      • 21.3.5 Task.Delay 方法
    • 21.4 GUI 程序中的异步操作(*)
    • 21.5 使用异步 Lambda 表达式(*)
    • 21.6 一个完整的 GUI 示例
    • 21.7 BackgroundWorker 类(*)
    • 21.8 并行循环(*)
    • 21.9 其他异步编程模式(*)
    • 21.10 BeginInvoke 和 EndInvoke(*)
    • 21.11 计时器(*)

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 计时器(*)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/345600.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

获得利润和成长,应采取什么步骤, 澳福认为只需坚持一点

大多数交易者通常会考虑在外汇交易中获取利润&#xff0c;但只有少数人会思考这样一个问题:为了获得利润和专业成长&#xff0c;应该采取什么步骤。像“外汇交易怎么赢利”这样的文章很受市场欢迎&#xff0c;但是很少有人在交易中使用这些文章中给出的建议&#xff0c;因为在生…

面试题:说一说多线程常见锁的策略 ?

文章目录 前言一、乐观锁和悲观锁1.1 定义1.2 生动有趣滴例子1.3 版本号机制 二、读写锁2.1 读写锁的由来2.2 生动有趣de例子2.3 ReentrantReadWriteLock 类 三、重量级锁与轻量级锁3.1 定义3.2 生动活泼の例子3.3 自旋锁&#xff08;Spin Lock&#xff09; 四、公平锁与非公平…

Spark原理——Shuffle 过程

Shuffle 过程 Shuffle过程的组件结构 从整体视角上来看, Shuffle 发生在两个 Stage 之间, 一个 Stage 把数据计算好, 整理好, 等待另外一个 Stage 来拉取 放大视角, 会发现, 其实 Shuffle 发生在 Task 之间, 一个 Task 把数据整理好, 等待 Reducer 端的 Task 来拉取 如果更细…

JAVA数组以及小练习

目录 数组的概述和静态初始化 数组的地址值和元素访问 数组的遍历 数组的动态初始化 数组练习 数组的概述和静态初始化 package 数组;public class array1 {public static void main(String[] args){//格式//静态初始化//数据类型 [] 数组名 new 数组类型[]{元素1&#xf…

0104 AJAX介绍

Ajax 的全称是 Asynchronous Javascript And XML &#xff08;异步 JavaScript 和 XML &#xff09;。 通俗的理解&#xff1a;在网页中利用 XMLHttpRequest 对象和服务器进行数据交互的方式&#xff0c;就是 Ajax Ajax 能让我们轻松实现网页与服务器之间的数据交互。 浏览器…

一杯干红葡萄酒的酿造

一杯干红葡萄酒的酿造 一、什么是干红葡萄酒&#xff1f; 干红葡萄酒是指葡萄酒在酿造后&#xff0c;酿酒原料(葡萄汁)中的糖分完全转化成酒精&#xff0c;残糖量小于或等于4.00/L的红葡萄酒。 干红葡萄酒按颜色分可以分为 1&#xff0c;白葡萄酒:选择用白葡萄或浅色果皮的酿…

【光波电子学】基于MATLAB的多模光纤模场分布的仿真分析

基于MATLAB的多模光纤模场分布的仿真分析 一、引言 &#xff08;1&#xff09;多模光纤的概念 多模光纤&#xff08;MMF&#xff09;是一种具有较大纤芯直径的光纤结构&#xff0c;其核心直径通常在10-50微米范围内。与单模光纤&#xff08;SMF&#xff09;相比&#xff0c;…

【C++刷题】位运算

【C刷题】位运算 一、二进制中最右侧的11、位1的个数&#xff08;1&#xff09;题目链接&#xff08;2&#xff09;解析&#xff08;3&#xff09;代码 2、比特位计数&#xff08;1&#xff09;题目链接&#xff08;2&#xff09;解析&#xff08;3&#xff09;代码 3、汉明距离…

Flink定制化功能开发,demo代码

前言&#xff1a; 这是一个Flink自定义开发的基础教学。本文将通过flink的DataStream模块API&#xff0c;以kafka为数据源&#xff0c;构建一个基础测试环境&#xff1b;包含一个kafka生产者线程工具&#xff0c;一个自定义FilterFunction算子&#xff0c;一个自定义MapFunctio…

统计学-R语言-4.2

文章目录 前言单变量数据的描述分析分类型数据频数表条形图饼图 数值型数据数值型数据数据的集中趋势--均值数据的集中趋势--众数 离散程度离散程度--极差离散程度--四分位数极差离散程度--方差离散程度--加权方差离散程度--标准差离散程度--变异系数 数据的形状数据的形状--偏…

《安富莱嵌入式周报》第330期:开源ECU模组,开源USB PD供电SMD回流焊,嵌入式系统开发C代码参考指南,旨在提升C语言编写的源码质量

周报汇总地址&#xff1a;嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 更新一期视频教程 BSP视频教程第29期&#xff1a;J1939协议栈CAN总线专题&#xff0c;源码框架&#xff0c;执行流程和…

【深度学习目标检测】十五、基于深度学习的口罩检测系统-含GUI和源码(python,yolov8)

YOLOv8是一种物体检测算法&#xff0c;是YOLO系列算法的最新版本。 YOLO&#xff08;You Only Look Once&#xff09;是一种实时物体检测算法&#xff0c;其优势在于快速且准确的检测结果。YOLOv8在之前的版本基础上进行了一系列改进和优化&#xff0c;提高了检测速度和准确性。…