当前位置: 首页 > news >正文

【C#】async和await 续

前言

在文章《async和await》中,我们观察到了一下客观的规律,但是没有讲到本质,而且还遗留了一个问题:

这篇文章中,我们继续看看这个问题如何解决!

问题遗留

我们再看看之前写的代码:

static public void TestWait2()
{var t = Task.Factory.StartNew(async () =>{Console.WriteLine("Start");await Task.Delay(3000);Console.WriteLine("Done");});t.Wait();Console.WriteLine("All done");}static public void TestWait3()
{var t = Task.Run(async () =>{Console.WriteLine("Start");await Task.Delay(3000);Console.WriteLine("Done");});t.Wait();Console.WriteLine("All done");
}

当时问题是,为啥 Task.Factory.StartNew 可以看到异步效果,而Task.Run中却是同步效果。
那其实是因为,Task.Factory.StartNew 返回的 t.Wait(); 它没卡住主线程,而Task.Run的 t.Wait();它卡住了。

那为啥,Task.Factory.StartNew没卡住呢?
这是应为 Task.Factory.StartNew 返回的变量 t 他是Task< Task >类型!

如果,Task.Run 返回的是Task类型,如果我们改成Task.Factory.StartNew,那么它 返回的类型就是Task<Task< int >>

在.Net4.0中提供一个Unwrap方法,用于将Task<Task< int>>解为Task< int>类型,所以如果代码改为:

static public async void Factory嵌套死等()
{Console.WriteLine($"Factory不嵌套死等{getID()}");var t = Task.Factory.StartNew(async() =>{Console.WriteLine($"Start{getID()}");await Task.Delay (1000);Console.WriteLine($"Done{getID()}");}).Unwrap();t.Wait();Console.WriteLine($"All done{getID()}");
}

那么此时 t.Wait(); 也能卡死主线程。

其实Task.Run(.net4.5引入) 是在 Task.Factory.StartNew(.net4.0引入) 之后出现的,Task.Run是为了简化Task.Factory.StartNew的使用。

t.Wait() 和 await t;

现在我从另一个角度分析问题。
使用 Task.Run,能不能达到异步的效果? 答案是肯定的!
不过,我们此时不应该使用 t.Wait(); 而是应该是 await t;

static public async void Run嵌套Await()
{Console.WriteLine($"Run嵌套Await{getID()}");var t = Task.Run(async () =>{Console.WriteLine($"Start{getID()}");await Task.Delay(1000);Console.WriteLine($"Done{getID()}");});await t;Console.WriteLine($"All done{getID()}");
}

在这里插入图片描述

这样的话就实现了异步效果。

await 是如何实现异步的

这里我们可以进一步分析一下。
“1” 是主线程的ID “5” 是 task 启的子线程 ID。
我发现All done 在 Done 后面执行的,这是应为 await t; 把主线程"遣返了"
而await t; 之后的代码(也就是All done 这句话的打印)是由子线程5接着完成的。

整个流程是这样的,当编译时,编译器看到了函数使用了 async 关键字,那么整个函数将被转换为一个带有状态机的函数,反编译后发现函数名称变为MoveNext。

当主线程执行到子函数时,遇到 await 那么此时 主线程就会返回(跳出整个子函数,去执行下一个函数),MoveNext呢就会切换状态机(由于状态机已经切换,下次MoveNext在被调用时,就会从await 处向下执行)。
不过,从现象看await 之后的代码,不是主线程调用了,而是Task的子线程。子线程会再次调用MoveNext,并且进入一个新的状态机。
这里就有一个结论,当主线程进入一个子函数,遇到await机会从函数直接返回,函数中以下的代码交给新的子线程执行。
为了证明一这一点,我又写了一个程序:

static public async Task AsyncInvoke()
{await Task.Run(() =>{Console.WriteLine($"This is 1 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);});Console.WriteLine($"1{getID()}");await Task.Run(() =>{Console.WriteLine($"This is 2 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);});Console.WriteLine($"2{getID()}");await Task.Run(() =>{Console.WriteLine($"This is 3 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);});Console.WriteLine($"3{getID()}");await Task.Run(() =>{Console.WriteLine($"This is 4 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);});Console.WriteLine($"4{getID()}");
}

执行效果如下:
在这里插入图片描述
你会发现不过一个函数里面有多少个await ,主线程遇到一个await就返回了,就跳出这个函数去执行其他的函数了。
函数剩下的await 后面的都是由子线程完成的!多个await 只是多个几个状态机而已。
所以在一个函数中,如果有个多个await ,除了第一个后面的都和主线程无关。

这里又出现了一个新的问题,为啥后面的线程ID都是5?这个其实不一定的,我重新跑了一次:
在这里插入图片描述
这次发现出现了两个子线程号 3 和 5,这是应为 Task 背后有个 线程池。Task 被翻译为任务,单纯的线程是指的Thread
Task 启动后,使用哪个线程是由背后的线程池提供,而这个线程池是由.net进行维护。包括回调什么时候发生都是由线程池中的一个线程通知Task对象!await 操作符 其实是 调用 Task对象的 ContinueWith,所以上面这段代码也可以这么写:

/// <summary>
/// 回调式写法
/// </summary>
public void TaskInvokeContinue()
{Task.Run(() =>{Console.WriteLine($"This is 1 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);}).ContinueWith(t =>{Console.WriteLine($"This is 2 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);}).ContinueWith(t =>{Console.WriteLine($"This is 3 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);}).ContinueWith(t =>{Console.WriteLine($"This is 4 ManagedThreadId={Thread.CurrentThread.ManagedThreadId.ToString("00")}");Thread.Sleep(1000);});//不太爽---nodejs---回调式写法,嵌套是要疯掉
}

这就进一步体现了 await 用同步的方式,写异步的代码。
能实现这个的原因就是,函数已经被改造成一个状态机了。

到这里,我就把上次坑给填上了!下次我们在一起掰扯掰扯Task的一些有意思的用法。

小结

我觉得最重要的一点就是:
主线程遇到一个await就返回了,如何理解这个返回?
返回就是跳出这个函数,和这个函数没有半毛钱关系了,去执行其他下面的函数了。
该函数剩下的await后面的部分 都是由线程池中的子线程完成的!
理解这一点,有助于我们对异步代码的编写!

2023年7月29日 更新 (一次Debug的分享)

昨天才写完这篇文章,今天就发现之前的写的一段代码有问题。没想到这么快就用上了~~(笑哭)

程序大概是这样的。我有一个主线程,里面有两个函数A和B,A和B实现了 async await 。
A和B里有一句 await tcpcli.SendAsync(str) 这句异步代码, 大致代码如下:

while(true)
{await  A(){....bool b = await tcpcli.SendAsync(str);}await  B(){....await tcpcli.SendAsync(str);}
}

正常情况下,这样没啥问题。程序都是正常跑。但是当Tcp服务那边反应延时的时候,就会出问题。
运行到 bool b = await tcpcli.SendAsync(str); 时,按照之前的结论,主线程都是直接返回的,就会直接执行B
然后再接着执行A,但是如果bool b = await tcpcli.SendAsync(str); 依然还在等待,之线程还是会返回的,
此时会再次开一个新的线程,导致多个线程并发,但是我这里其他逻辑并发的话是会有问题的(比如写Modbus的一个寄存器)。
所以,一旦 tcpcli.SendAsync(str)卡住了,逻辑就出问题了!

既然逻辑不能并发,我当时为啥不直接用同步的方式呢?其实原因是当时我不知道如何用同步的方式获取返回值。
我当时 调用 tcpcli.SendAsync(str).Wait(); 时发现这个Wait()返回值是空,但是我又需要返回值,所以就用了
bool b = await tcpcli.SendAsync(str); 那其实如果想用同步的方式获取返回值,应该使用:
bool b = tcpcli.SendAsync(str).GetAwaiter().GetResult();
所以,最后改程序为:

while(true)
{await  A(){....bool b = tcpcli.SendAsync(str).GetAwaiter().GetResult();}await  B(){....tcpcli.SendAsync(str).Wait();}
}

相关文章:

【C#】async和await 续

前言 在文章《async和await》中&#xff0c;我们观察到了一下客观的规律&#xff0c;但是没有讲到本质&#xff0c;而且还遗留了一个问题: 这篇文章中&#xff0c;我们继续看看这个问题如何解决! 我们再看看之前写的代码&#xff1a; static public void TestWait2() {var t…...

【Matlab】基于粒子群优化算法优化BP神经网络的数据回归预测(Excel可直接替换数据)

【Matlab】基于粒子群优化算法优化 BP 神经网络的数据回归预测(Excel可直接替换数据) 1.模型原理2.数学公式3.文件结构4.Excel数据5.分块代码5.1 fun.m5.2 main.m6.完整代码6.1 fun.m6.2 main.m7.运行结果1.模型原理 基于粒子群优化算法(Particle Swarm Optimization, PSO)…...

QPainter绘制雷达界面

文章目录 功能实现定义的结构体定义的函数效果图gitee源码链接 功能实现 相较于上一版&#xff0c;这一版添加的功能有&#xff1a; 1、自适应窗口 2、扫描方式&#xff08;圆周扫描、扇形扫描&#xff08;指定起始角度和结束角度&#xff09;&#xff09; 3、扫描方向&#x…...

flutter:BottomNavigationBar和TabBar

区别 BottomNavigationBarr和TabBar都是用于创建导航栏的组件&#xff0c;但它们有一些区别。 位置不同&#xff1a;BottomNavigationBar通常位于屏幕底部&#xff0c;用于主要导航&#xff1b;而TabBar通常位于屏幕顶部或底部&#xff0c;用于切换不同的视图或页面。 样式不…...

【图论】Prim算法

一.介绍 Prim算法是一种用于解决最小生成树问题的贪心算法。最小生成树问题是指在一个连通无向图中找到一个生成树&#xff0c;使得树中所有边的权重之和最小。 Prim算法的基本思想是从一个起始顶点开始&#xff0c;逐步扩展生成树&#xff0c;直到覆盖所有顶点。具体步骤如下…...

第九十二回 在Flutter中解析JSON数据

文章目录 概念介绍解析方法convert库插件工具 示例代码经验总结 我们在上一章回中介绍了"对dio库进行封装"相关的内容&#xff0c;本章回中将介绍 如何在Flutter中解析JSON数据.闲话休提&#xff0c;让我们一起Talk Flutter吧。 概念介绍 我们在前面章回中介绍了通…...

银河麒麟安装mysql数据库(mariadb)-银河麒麟安装JDK-银河麒麟安装nginx(附安装包)

银河麒麟离线全套安装教程&#xff08;手把手教程&#xff09; 1.银河麒麟服务器系统安装mysql数据库&#xff08;mariadb&#xff09; 2.银河麒麟桌面系统安装mysql数据库&#xff08;mariadb&#xff09; 3.银河麒麟服务器系统安装JDK 4.银河麒麟桌面系统安装JDK 5.银河麒麟…...

文件上传

js绕过 打开网页尝试上传一句话木马&#xff0c;发现只能上传图片文件 审计源代码&#xff0c;发现使用一个checkfile函数js对文件类型进行了屏蔽 于是我们修改网页代码&#xff0c;去除返回值的检查函数 checkFile() 上传成功&#xff0c;使用蚁剑连接 连接成功 .htaccess绕…...

tinkerCAD案例:22. Backpack Zipper Pull 背包拉链头

tinkerCAD案例&#xff1a;21. Custom Stamp 定制印章 原文 tinkerCAD案例&#xff1a;22. Backpack Zipper Pull 背包拉链头 Lesson Overview: 课程概述&#xff1a; Now we’re going to make a zipper pull! 现在我们要做一个拉链头&#xff01; Your backpack, howev…...

Unity 性能优化四:UI耗时函数、资源加载、卸载API

UI耗时函数 1.1 Canvas.SendWillRenderCanvases 这个函数是由于自身UI的更新&#xff0c;产生的耗时 1. 这里更新的是vertex 属性&#xff0c;比如 color、tangent、position、uv&#xff0c;修改recttransform的position、scale&#xff0c;rotation并不会导致顶点属性改变…...

【Linux】用户相关内容

如果命令ll 出现以上信息&#xff0c;UID为具体的数字&#xff0c;代表之前UID为502的用户被删除了。 更改目录或文件所属用户和所属组 在Linux中&#xff0c;创建一个文件时&#xff0c;该文件的拥有者都是创建该文件的用户。 更改所属用户 chown 用户名 文件名/目录名 更…...

基于多场景的考虑虑热网网损的太阳能消纳能力评估研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

【动态规划part10】| 121.买卖股票的最佳时机、122.买卖股票的最佳时机II

目录 &#x1f388;LeetCode121. 买卖股票的最佳时机 &#x1f388;LeetCode122.买卖股票的最佳时机II &#x1f388;LeetCode121. 买卖股票的最佳时机 链接&#xff1a;121.买卖股票的最佳时机 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定…...

java 页面html常用写法总结

​(注意&#xff1a;本文章默认base html中已经引入bootstrap.min.css、style.css等css样式) input &#xff1a;输入标签 <#input required"必填" id"cycle" name"周期" underline"true" style"width:75%" itype&quo…...

阿里云服务器全方位介绍_优势_使用_租用费用详解

阿里云服务器全方位介绍包括云服务器ECS优势、云服务器租用价格、云服务器使用场景及限制说明&#xff0c;阿里云服务器网分享云服务器ECS介绍、个人和企业免费试用、云服务器活动、云服务器ECS规格、优势、功能及应用场景详细你说明&#xff1a; 目录 什么是云服务器ECS&…...

【Kafka】常用操作

1、基本概念 1. 消息&#xff1a; Kafka是一个分布式流处理平台&#xff0c;它通过消息进行数据的传输和存储。消息是Kafka中的基本单元&#xff0c;可以包含任意类型的数据。 2. 生产者&#xff08;Producer&#xff09;&#xff1a; 生产者负责向Kafka主题发送消息。它将消息…...

【Spring框架】SpringBoot配置文件

目录 配置文件作用application.properties中午乱码问题&#xff1a;配置文件里面的配置类型分类SpringBoot热部署properties基本语法properties配置文件的优缺点&#xff1a;yml配置文件说明yml基本语法配置对象properties VS yml 配置文件作用 整个项⽬中所有重要的数据都是在…...

部署问题集合(十八)Windows环境下使用两个Tomcat

下载Tomcat Tomcat镜像下载地址&#xff1a;https://mirrors.cnnic.cn/apache/tomcat/进入如下地址&#xff1a;zip的是压缩版&#xff0c;exe是安装版 修改第二个Tomcat配置文件 第一步&#xff1a;编辑conf/server.xml文件&#xff0c;修改三个端口&#xff0c;有些版本改…...

数据结构问答8

查找 1. 一些基本概念 关键字:能唯一标识该元素 查找:给定值k,在含n个元素的表中找出关键字==k的元素。找到返回其位置信息,否则返回-1。 动、静态查找表:查找同时对表进行修改(插入、删除等),相应的表为动态,否则为静态。 内、外查找:整个查找过程在内存中进行…...

行为型设计模式之观察者模式【设计模式系列】

系列文章目录 C技能系列 Linux通信架构系列 C高性能优化编程系列 深入理解软件架构设计系列 高级C并发线程编程 设计模式系列 期待你的关注哦&#xff01;&#xff01;&#xff01; 现在的一切都是为将来的梦想编织翅膀&#xff0c;让梦想在现实中展翅高飞。 Now everythi…...

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻

在如今就业市场竞争日益激烈的背景下&#xff0c;越来越多的求职者将目光投向了日本及中日双语岗位。但是&#xff0c;一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧&#xff1f;面对生疏的日语交流环境&#xff0c;即便提前恶补了…...

深入剖析AI大模型:大模型时代的 Prompt 工程全解析

今天聊的内容&#xff0c;我认为是AI开发里面非常重要的内容。它在AI开发里无处不在&#xff0c;当你对 AI 助手说 "用李白的风格写一首关于人工智能的诗"&#xff0c;或者让翻译模型 "将这段合同翻译成商务日语" 时&#xff0c;输入的这句话就是 Prompt。…...

进程地址空间(比特课总结)

一、进程地址空间 1. 环境变量 1 &#xff09;⽤户级环境变量与系统级环境变量 全局属性&#xff1a;环境变量具有全局属性&#xff0c;会被⼦进程继承。例如当bash启动⼦进程时&#xff0c;环 境变量会⾃动传递给⼦进程。 本地变量限制&#xff1a;本地变量只在当前进程(ba…...

React Native 导航系统实战(React Navigation)

导航系统实战&#xff08;React Navigation&#xff09; React Navigation 是 React Native 应用中最常用的导航库之一&#xff0c;它提供了多种导航模式&#xff0c;如堆栈导航&#xff08;Stack Navigator&#xff09;、标签导航&#xff08;Tab Navigator&#xff09;和抽屉…...

2021-03-15 iview一些问题

1.iview 在使用tree组件时&#xff0c;发现没有set类的方法&#xff0c;只有get&#xff0c;那么要改变tree值&#xff0c;只能遍历treeData&#xff0c;递归修改treeData的checked&#xff0c;发现无法更改&#xff0c;原因在于check模式下&#xff0c;子元素的勾选状态跟父节…...

leetcodeSQL解题:3564. 季节性销售分析

leetcodeSQL解题&#xff1a;3564. 季节性销售分析 题目&#xff1a; 表&#xff1a;sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...

Axios请求超时重发机制

Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式&#xff1a; 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

Spring AI与Spring Modulith核心技术解析

Spring AI核心架构解析 Spring AI&#xff08;https://spring.io/projects/spring-ai&#xff09;作为Spring生态中的AI集成框架&#xff0c;其核心设计理念是通过模块化架构降低AI应用的开发复杂度。与Python生态中的LangChain/LlamaIndex等工具类似&#xff0c;但特别为多语…...

MySQL用户和授权

开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务&#xff1a; test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...