前言

Talk is cheap, Show you the code first!

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3. Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  4. AsyncMethod();
  5. Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  6. }
  7.  
  8. private async Task AsyncMethod()
  9. {
  10. var ResultFromTimeConsumingMethod = TimeConsumingMethod();
  11. string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
  12. Console.WriteLine(Result);
  13. //返回值是Task的函数可以不用return
  14. }
  15.  
  16. //这个函数就是一个耗时函数,可能是IO操作,也可能是cpu密集型工作。
  17. private Task<string> TimeConsumingMethod()
  18. {
  19. var task = Task.Run(()=> {
  20. Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  21. Thread.Sleep();
  22. Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  23. return "Hello I am TimeConsumingMethod";
  24. });
  25.  
  26. return task;
  27. }

我靠,这么复杂!!!竟然有三个函数!!!竟然有那么多行!!!
别着急,慢慢看完,最后的时候你会发现使用async/await真的炒鸡优雅。

异步方法的结构

上面是一个的使用async/await的例子(为了方便解说原理我才写的这样复杂的)。
使用async/await能非常简单的创建异步方法,防止耗时操作阻塞当前线程。
使用async/await来构建的异步方法,逻辑上主要有下面三个结构:

调用异步方法

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3. Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  4. AsyncMethod();//这个方法就是异步方法,异步方法的调用与一般方法完全一样
  5. Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  6. }

注意:微软建议异步方法的命名是在方法名后添加Aysnc后缀,示例是我为了读起来方便做成了前缀,在真正构建异步方法的时候请注意用后缀。(好吧我承认是我忘记了,然后图片也都截好了再修改太麻烦了。。。。就懒得重新再修改了)
异步方法的返回类型只能是voidTaskTask<TResult>。示例中异步方法的返回值类型是Task

另外,上面的AsyncMethod()会被编译器提示报警,如下图:

因为是异步方法,所以编译器提示在前面使用await关键字,这个后面再说,为了不引入太多概念导致难以理解暂时就先这么放着。

异步方法本体

  1. private async Task AsyncMethod()
  2. {
  3. var ResultFromTimeConsumingMethod = TimeConsumingMethod();
  4. string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
  5. Console.WriteLine(Result);
  6. //返回值是Task的函数可以不用return
  7. }

异步方法必须用async来修饰,表明这个方法是异步的,声明的方法的返回类型必须为:void或Task或Task。方法内部必须含有await修饰的方法,如果方法内部没有await关键字修饰的表达式,哪怕函数被async修饰也只能算作同步方法,执行的时候也是同步执行的。

被await修饰的只能是Task或者Task<TResule>类型,通常情况下是一个返回类型是Task/Task<TResult>的方法,当然也可以修饰一个Task/Task<TResult>变量,await只能出现在已经用async关键字修饰的异步方法中。上面代码中就是修饰了一个变量ResultFromTimeConsumingMethod

关于被修饰的对象,也就是返回值类型是TaskTask<TResult>函数或者Task/Task<TResult>类型的变量:如果是被修饰对象的前面用await修饰,那么返回值实际上是void或者TResult(示例中ResultFromTimeConsumingMethodTimeConsumingMethod()函数的返回值,也就是Task<string>类型,当ResultFromTimeConsumingMethod在前面加了await关键字后 await ResultFromTimeConsumingMethod实际上完全等于 ResultFromTimeConsumingMethod.Result)。如果没有await,返回值就是Task或者Task<TResult>

耗时函数

  1. //这个函数就是一个耗时函数,可能是IO密集型操作,也可能是cpu密集型工作。
  2. private Task<string> TimeConsumingMethod()
  3. {
  4. var task = Task.Run(()=> {
  5. Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  6. Thread.Sleep();
  7. Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  8. return "Hello I am TimeConsumingMethod";
  9. });
  10.  
  11. return task;
  12. }

这个函数才是真正干活的(为了让逻辑层级更分明,我把这部分专门做成了一个函数,在后面我会精简一下直接放到异步函数中,毕竟活在哪都是干)。

在示例中是一个CPU密集型的工作,我另开一线程让他拼命干活干5s。如果是IO密集型工作比如文件读写等可以直接调用.Net提供的类库,对于这些类库底层具体怎么实现的?是用了多线程还是DMA?或者是多线程+DMA?这些问题我没有深究但是从表象看起来和我用Task另开一个线程去做耗时工作是一样的。

await只能修饰Task/Task<TResult>类型,所以这个耗时函数的返回类型只能是Task/Task<TResult>类型。

总结:有了上面三个结构就能完成使用一次异步函数。

async/await异步函数的原理

在开始讲解这两个关键字之前,为了方便,对某些方法做了一些拆解,拆解后的代码块用代号指定:

上图对示例代码做了一些指定具体就是:

  • Caller代表调用方函数,在上面的代码中就是button1_Click函数。
  • CalleeAsync代表被调用函数,因为代码中被调用函数是一个异步函数,按照微软建议的命名添加了Async后缀,在上面示例代码中就是AsyncMethod()函数。
  • CallerChild1代表调用方函数button1_Click在调用异步方法CalleeAsync之前的那部分代码。
  • CallerChild2代表调用方函数button1_Click在调用异步方法CalleeAsync之后的那部分代码。
  • CalleeChild1代表被调用方函数AsyncMethod遇到await关键字之前的那部分代码。
  • CalleeChild2代表被调用方函数AsyncMethod遇到await关键字之后的那部分代码。
  • TimeConsumingMethod是指被await修饰的那部分耗时代码(实际上我代码中也是用的这个名字来命名的函数)

示例代码的执行流程


为了方便观看我模糊掉了对本示例没有用的输出。
这里涉及到了两个线程,线程ID分别是1和3。

Caller函数被调用,先执行CallerChild1代码,这里是同步执行与一般函数一样,然后遇到了异步函数CalleeAsync。

在CalleeAsync函数中有await关键字,await的作用是打分裂点。

编译器会把整个函数(CalleeAsync)从这里分裂成两个函数。await关键字之前的代码作为一个函数(按照我上面定义的指代,下文中就叫这部分代码CalleeChild1)await关键字之后的代码作为一个函数(CalleeChild2)。

CalleeChild1在调用方线程执行(在示例中就是主线程Thread1),执行到await关键字之后,另开一个线程耗时工作在Thread3中执行,然后立即返回。这时调用方会继续执行下面的代码CallerChild2(注意是Caller不是Callee)。

在CallerChild2被执行期间,TimeConsumingMethod也在异步执行(可能是在别的线程也可能是CPU不参与操作直接DMA的IO操作)。

当TimeConsumingMethod执行结束后,CalleeChild2也就具备了执行条件,而这个时候CallerChild2可能执行完了也可能没有,由于CallerChild2与CalleeChild2都会在Caller的线程执行,这里就会有冲突应该先执行谁,编译器会在合适的时候在Caller的线程执行这部分代码。示意图如下:

请注意,CalleeChild2在上图中并没有画任何箭头,因为这部分代码的执行是由编译器决定的,暂时无法具体描述是什么时候执行。

总结一下:

整个流程下来,除了TimeConsumingMethod函数是在Thread3中执行的,剩余代码都是在主线程Thread1中执行的.

也就是说异步方法运行在当前同步上下文中,只有激活的时候才占用当前线程的时间,异步模型采用时间片轮转来实现(这一点我没考证,仅作参考)。

你也许会说,明明新加了一个Thread3线程怎么能说是运行在当前的线程中呢?这里说的异步方法运行在当前线程上的意思是由CalleeAsync分裂出来的CalleeChild1和CalleeChild2的确是运行在Thread1上的。

带返回值的异步函数

之前的示例代码中异步函数是没有返回值的,作为理解原理足够了,但是在实际应用场景中,带返回值的应用才是最常用的。那么,上代码:

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3. Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  4. var ResultTask = AsyncMethod();
  5. Console.WriteLine(ResultTask.Result);
  6. Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  7. }
  8.  
  9. private async Task<string> AsyncMethod()
  10. {
  11. var ResultFromTimeConsumingMethod = TimeConsumingMethod();
  12. string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId;
  13. Console.WriteLine(Result);
  14. return Result; //返回类型为Task<T>的异步方法中应有返回T类型的return 语句。
  15. }
  16.  
  17. //这个函数就是一个耗时函数,可能是IO操作,也可能是cpu密集型工作。
  18. private Task<string> TimeConsumingMethod()
  19. {
  20. var task = Task.Run(()=> {
  21. Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  22. Thread.Sleep();
  23. Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  24. return "Hello I am TimeConsumingMethod";
  25. });
  26.  
  27. return task;
  28. }

主要更改的地方在这里:

按理说没错吧?然而,这代码一旦执行就会卡死。

死锁

是的,死锁。分析一下为什么:

按照之前我划定的代码块指定,在添加了新代码后CallerChild2与CalleeChild2的划分如上图。

这两部分代码块都是在同一个线程上执行的,也就是主线程Thread1,而且通常情况下CallerChild2是会早于CalleeChild2执行的(毕竟CalleeChild2得在耗时代码块执行之后执行)。

Console.WriteLine(ResultTask.Result);(CallerChild2)其实是在请求CalleeChild2的执行结果,此时明显CalleeChild2还没有结束没有return任何结果,那Console.WriteLine(ResultTask.Result);就只能阻塞Thread1等待,直到CalleeChild2有结果。

然而问题就在这,CalleeChild2也是在Thread1上执行的,此时CallerChild2一直占用Thread1等待CalleeChild2的结果,耗时程序结束后轮到CalleeChild2执行的时候CalleeChild2又因Thread1被CallerChild2占用而抢不到线程,永远无法return,那么CallerChild2就会永远等下去,这就造成了死锁。

解决办法有两种一个是把Console.WriteLine(ResultTask.Result);放到一个新开线程中等待(个人觉得这方法有点麻烦,毕竟要新开线程),还有一个方法是把Caller也做成异步方法:

ResultTask.Result变成了ResultTask 的原因上面也说了,await修饰的Task/Task<TResult>得到的是TResult。

之所以这样就能解决问题是因为嵌套了两个异步方法,现在的Caller也成了一个异步方法,当Caller执行到await后直接返回了(await拆分方法成两部分),CalleeChild2执行之后才轮到Caller中await后面的代码块(Console.WriteLine(ResultTask.Result);)。

另外,把Caller做成异步的方法也解决了一开始的那个警告,还记得么?

这样没省多少事啊?

到现在,你可能会说:使用async/await不比直接用Task.Run()来的简单啊?比如我用TaskTaskContinueWith方法也能实现:

  1. private void button1_Click(object sender, EventArgs e)
  2. {
  3. var ResultTask = Task.Run(()=> {
  4. Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  5. Thread.Sleep();
  6. Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId);
  7. return "Hello I am TimeConsumingMethod";
  8. });
  9.  
  10. ResultTask.ContinueWith(OnDoSomthingIsComplete);
  11.  
  12. }
  13.  
  14. private void OnDoSomthingIsComplete(Task<string> t)
  15. {
  16. Action action = () => {
  17. textBox1.Text = t.Result;
  18. };
  19. textBox1.Invoke(action);
  20. Console.WriteLine("Continue Thread ID :" + Thread.CurrentThread.ManagedThreadId);
  21. }

是的,上面的代码也能实现。但是,async/await的优雅的打开方式是这样的:

  1. private async void button1_Click(object sender, EventArgs e)
  2. {
  3. var t = Task.Run(() => {
  4. Thread.Sleep();
  5. return "Hello I am TimeConsumingMethod";
  6. });
  7. textBox1.Text = await t;
  8. }

看到没,惊不惊喜,意不意外,寥寥几行就搞定了,不用再多写那么多函数,使用起来也很灵活。最让人头疼的跨线程修改控件的问题完美解决了,再也不用使用Invoke了,因为修改控件的操作压根就是在原来的线程上做的,还能不阻塞UI。

参考:
https://www.cnblogs.com/OpenCoder/p/4434574.html
https://www.cnblogs.com/zhili/archive/2013/05/15/Csharp5asyncandawait.html
http://www.cnblogs.com/heyuquan/archive/2013/04/26/3045827.html
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/index

作者:feipeng8848

出处:https://www.cnblogs.com/feipeng8848/p/10188871.html

彻底搞懂 C# 的 async/await的更多相关文章

  1. Async/Await是这样简化JavaScript代码的

    译者按: 在Async/Await替代Promise的6个理由中,我们比较了两种不同的异步编程方法:Async/Await和Promise,这篇博客将通过示例代码介绍Async/Await是如何简化J ...

  2. C# 彻底搞懂async/await

    前言 Talk is cheap, Show you the code first! private void button1_Click(object sender, EventArgs e) { ...

  3. 答应我,这次必须搞懂!痛点难点Promise。(小点心async/await,基于Promise的更优方案)

    Promise 出现的原因 在 Promise 出现以前,我们处理一个异步网络请求,大概是这样: // 请求 代表 一个异步网络调用. // 请求结果 代表网络请求的响应. 请求1(function( ...

  4. 小白终于弄懂了:c#从async/await到Task再到Thread

    1. 为什么会有/怎么解决: async/await的无限嵌套 public async Task<int> myFuncAsync1() { //some code int num = ...

  5. 进阶篇:以IL为剑,直指async/await

    接上篇:30分钟?不需要,轻松读懂IL,这篇主要从IL入手来理解async/await的工作原理. 先简单介绍下async/await,这是.net 4.5引入的语法糖,配合Task使用可以非常优雅的 ...

  6. promise async await使用

    1.Promise (名字含义:promise为承诺,表示其他手段无法改变) Promise 对象代表一个异步操作,其不受外界影响,有三种状态: Pending(进行中.未完成的) Resolved( ...

  7. 浅谈C#中的 async await 以及对线程相关知识的复习

    C#5.0以后新增了一个语法糖,那就是异步方法async await,之前对线程,进程方面的知识有过较为深入的学习,大概知道这个概念,我的项目中实际用到C#异步编程的场景比较少,就算要用到一般也感觉T ...

  8. promise和async await的区别

    在项目中第一次遇到async await的这种异步写法,来搞懂它 项目场景 :点击登录按钮后执行的事件,先进行表单校验 this.$refs.loginFormRef.validate(element ...

  9. C# async/await 使用总结

    今天搞这两个关键字搞得有点晕,主要还是没有彻底理解其中的原理. 混淆了一个调用异步方法的概念: 在调用异步方法时,虽然方法返回一个 Task,但是其中的代码已经开始执行.该方法在调用时,即刻执行了一部 ...

随机推荐

  1. 【翻译】asp.net core2.0中的token认证

    原文地址:https://developer.okta.com/blog/2018/03/23/token-authentication-aspnetcore-complete-guide token ...

  2. synchronized和lock有什么区别?

    一.原始构成 synchronized是关键字属于JVM层面,monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖monitor对象只有在同步代码块和同 ...

  3. Linux下一些简单常用命令的总结

    Cent os常用命令 查看当前目录信息: l s a:显示所有包括隐藏文件 l :列表显示数据 h:显示数据大小 查看命令帮助: XXX --help[比如:l shel p] 清除控制台信息: c ...

  4. springdata 动态查询之排序

  5. C博客作业01--分支、顺序结构

    1.本章学习总结 1.1 思维导图 1.2 本章学习体会及代码量学习体会 1.2.1 学习体会 在暑假已经有初步接触c语言,所以在学习c语言的开始会比较轻松,但仍然解题时候步骤太过于繁琐,简单的题目复 ...

  6. cv2.matchTemplate()函数的应用,匹配图片后画出矩形

    import cv2 as cv import numpy as np """ matchTemplate(): 参数image:待搜索的图像(大图) 参数temple: ...

  7. 01Design and Analysis Algorithm Using Python-程振波

    1.(p14)比较两个数的大小 a = int(input('num:')) b = int(input('num:')) def getMax(a,b): if a>b : print('Th ...

  8. 虚拟机 模拟centos 7 系统安装

    Cnetos 服务器安装过程  1.制作U盘启动器 网上有很多制作U盘启动的教程,这里就不详细说了 2.用U盘启动电脑进入安装界面 3.开始安装 等待片刻后,正常的应该会进入语言选择界面了. 选择中文 ...

  9. MFC:位图和图标的设置

    一. 图标的设置 加载图标   API函数:AfxGetApp()->LoadIconW(); 2. 显示图标 API函数:SetClassLong(); 函数原型:DWORD WINAPI S ...

  10. 逆FizzBuzz问题求最短序列

    问题描述 FizzBuzz问题:一个大于0的自然数能整除3,将输出“Fizz”:能整除5,将输出“Buzz”:能整除3和5,将输出“FizzBuzz”:否则输出自己. 逆FizzBuzz问题最短序列: ...