当前位置: 首页 > 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();}
}

http://www.lryc.cn/news/100514.html

相关文章:

  • 【Matlab】基于粒子群优化算法优化BP神经网络的数据回归预测(Excel可直接替换数据)
  • QPainter绘制雷达界面
  • flutter:BottomNavigationBar和TabBar
  • 【图论】Prim算法
  • 第九十二回 在Flutter中解析JSON数据
  • 银河麒麟安装mysql数据库(mariadb)-银河麒麟安装JDK-银河麒麟安装nginx(附安装包)
  • 文件上传
  • tinkerCAD案例:22. Backpack Zipper Pull 背包拉链头
  • Unity 性能优化四:UI耗时函数、资源加载、卸载API
  • 【Linux】用户相关内容
  • 基于多场景的考虑虑热网网损的太阳能消纳能力评估研究(Matlab代码实现)
  • 【动态规划part10】| 121.买卖股票的最佳时机、122.买卖股票的最佳时机II
  • java 页面html常用写法总结
  • 阿里云服务器全方位介绍_优势_使用_租用费用详解
  • 【Kafka】常用操作
  • 【Spring框架】SpringBoot配置文件
  • 部署问题集合(十八)Windows环境下使用两个Tomcat
  • 数据结构问答8
  • 行为型设计模式之观察者模式【设计模式系列】
  • vue2企业级项目(四)
  • (树) 剑指 Offer 26. 树的子结构 ——【Leetcode每日一题】
  • Linuxcnc-ethercat从入门到放弃(1)、环境搭建
  • 14.Netty源码之模拟简单的HTTP服务器
  • 万年历【小游戏】(Java课设)
  • ad+硬件每日学习十个知识点(9)23.7.20
  • ipmitool 配置BMC的ip
  • C++设计模式::小结(creation)
  • 运维工程师第一阶段windows的学习
  • Docker复习
  • 华为OD机考--食堂供餐--带答案