在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
前言Talk is cheap, Show you the code first! private void button1_Click(object sender, EventArgs e) { Console.WriteLine("111 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); AsyncMethod(); Console.WriteLine("222 balabala. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); } private async Task AsyncMethod() { var ResultFromTimeConsumingMethod = TimeConsumingMethod(); string Result = await ResultFromTimeConsumingMethod + " + AsyncMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId; Console.WriteLine(Result); //返回值是Task的函数可以不用return } //这个函数就是一个耗时函数,可能是IO操作,也可能是cpu密集型工作。 private Task<string> TimeConsumingMethod() { var task = Task.Run(()=> { Console.WriteLine("Helo I am TimeConsumingMethod. My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); Thread.Sleep(5000); Console.WriteLine("Helo I am TimeConsumingMethod after Sleep(5000). My Thread ID is :" + Thread.CurrentThread.ManagedThreadId); return "Hello I am TimeConsumingMethod"; }); return task; } 我靠,这么复杂!!!竟然有三个函数!!!竟然有那么多行!!! 别着急,慢慢看完,最后的时候你会发现使用 异步方法的结构上面是一个的使用 调用异步方法异步方法的返回类型只能是 另外,上面的AsyncMethod()会被编译器提示报警,如下图: 异步方法本体被await修饰的只能是 关于被修饰的对象,也就是返回值类型是 耗时函数在示例中是一个CPU密集型的工作,我另开一线程让他拼命干活干5s。如果是IO密集型工作比如文件读写等可以直接调用.Net提供的类库,对于这些类库底层具体怎么实现的?是用了多线程还是DMA?或者是多线程+DMA?这些问题我没有深究但是从表象看起来和我用Task另开一个线程去做耗时工作是一样的。
总结:有了上面三个结构就能完成使用一次异步函数。 async/await异步函数的原理在开始讲解这两个关键字之前,为了方便,对某些方法做了一些拆解,拆解后的代码块用代号指定:
示例代码的执行流程
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上的。 带返回值的异步函数之前的示例代码中异步函数是没有返回值的,作为理解原理足够了,但是在实际应用场景中,带返回值的应用才是最常用的。那么,上代码:
死锁是的,死锁。分析一下为什么: 按照之前我划定的代码块指定,在添加了新代码后CallerChild2与CalleeChild2的划分如上图。 这两部分代码块都是在同一个线程上执行的,也就是主线程Thread1,而且通常情况下CallerChild2是会早于CalleeChild2执行的(毕竟CalleeChild2得在耗时代码块执行之后执行)。
然而问题就在这,CalleeChild2也是在Thread1上执行的,此时CallerChild2一直占用Thread1等待CalleeChild2的结果,耗时程序结束后轮到CalleeChild2执行的时候CalleeChild2又因Thread1被CallerChild2占用而抢不到线程,永远无法return,那么CallerChild2就会永远等下去,这就造成了死锁。 解决办法有两种一个是把 之所以这样就能解决问题是因为嵌套了两个异步方法,现在的Caller也成了一个异步方法,当Caller执行到await后直接返回了(await拆分方法成两部分),CalleeChild2执行之后才轮到Caller中await后面的代码块( 另外,把Caller做成异步的方法也解决了一开始的那个警告,还记得么? 这样没省多少事啊?到现在,你可能会说:使用 参考: |
请发表评论