浅谈.Net异步编程的前世今生----异步函数篇(完结)

219305a929a8c0536c5b2d24af1fec3b.png

前言

上一篇我们着重讲解了TPL任务并行库,可以看出TPL已经很符合现代API的特性:简洁易用。但它的不足之处在于,使用者难以理解程序的实际执行顺序。

为了解决这些问题,在C# 5.0中,引入了新的语言特性,被称为异步函数(asynchronous function)。对应的.Net版本为.Net Framework 4.5。

最后一个异步编程模型:异步函数

b2f703b380fd408839da7d5ea8b44732.png

概述

由于异步函数为语言特性的实现,因此它的本质依然属于TPL模型,但提供了更高级别的抽象,真正简化了异步编程。抽象可以隐藏主要的实现细节,使得开发人员无需考虑许多重要的事情,从而达到简化的效果。

在本文中,我们主要会讲解异步函数的声明和使用方式,以及在多种场景下使用异步函数,处理异常等。

f1538cd3557dc8177ddddbd637254d7a.png

声明异步函数

声明异步函数的方法很简单,只需使用async关键字标注任意一个方法即可。需要注意的是,如果只使用了async标注方法,而方法内部未使用await,会导致编译警告,如图所示:

aa2a617e4f59558bf6a4561ba69f1a12.png

另一个重要的事实是,异步函数必须返回Task或Task<T>类型。也可使用async void,但不推荐,若使用async void方式, 异常处理及跟踪将不由TPL模型处理,而是会直接在SynchronizationContext上引发,这样会引起整个进程的崩溃。因此通常会在UI层处理事件时,才会使用async void方式。

改写后相关代码示例如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace asyncDemo
{public class Utils{public async Task<string> GetStringAsync(){await Task.Delay(TimeSpan.FromSeconds(2));return "Hello World!";}}
}

这里我们执行完await调用的代码行后,会立即返回,而不是阻塞两秒,如果是同步执行则结果相反。当执行完await操作后,TPL会立即将工作线程放回线程池,我们的程序会进行异步等待。直到2秒后,我们又一次从线程池中得到工作线程,并继续运行其中剩余的异步方法。这样就允许我们在等待2秒时,可以重用工作线程来做其他事,提升了应用程序的可伸缩性。

事实上,异步函数在编译器后台会被编译成复杂的程序结构,一般称之为迭代器。迭代器的内部是一种状态机,由于状态机的概念理解较为复杂,因此这里不再赘述。所以我们在日常编写代码时,并不需要将每一个方法都标记为async,尤其是并不需要使用异步的方法。通过上述概念可知,滥用async会导致编译器编译时生成大量的迭代器,会有显著的性能损失。

ce9692ad6b8f88c0f9a6602e76ebf220.png

获取异步任务结果

既然我们已经了解了async-await本质上依然为TPL模型,那么在使用TPL和await操作符获取异步结果中有什么不同呢?此处我们可以通过实验来探究。

如图所示,我们分别使用Task和await执行:

0a18e34492a953dddc56604bc1e01bcc.png

二者都调用了同一个异步函数打印当前线程的Id和状态。

在第一个中启动了一个任务,运行2秒后返回关于工作线程的信息。我们还定义了一个后续操作,用于在异步操作完成后,打印出操作结果;另一个后续操作用于有错误发生时,打印异常信息。最终返回一个代表其中一个后续操作任务的任务,并在Main中等待其执行完成。

而在第二个中,我们直接使用await对任务进行操作,获取异步执行的结果,同时使用try-catch代码块来捕获可能发生的异常,这和我们编写同步方法的代码风格是一致的,简化了程序编写的复杂度。实际上在await之后编译器创建了一个任务及后续操作,并处理了可能发生的异常信息。

相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace asyncDemo
{class Program{static void Main(string[] args){Task t = AsyncTPL();t.Wait();t = AsyncAwait();t.Wait();Console.Read();}static Task AsyncTPL(){Task<string> t = GetInfoAsync("任务1");Task t2 = t.ContinueWith(x => Console.WriteLine(t.Result), TaskContinuationOptions.NotOnFaulted);Task t3 = t.ContinueWith(x => Console.WriteLine(t.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);return Task.WhenAny(t2, t3);}async static Task AsyncAwait(){try{string result = await GetInfoAsync("任务2");Console.WriteLine(result);}catch (Exception ex){Console.WriteLine(ex);}}async static Task<string> GetInfoAsync(string name){await Task.Delay(TimeSpan.FromSeconds(2));return $"{name}的线程Id为:{Thread.CurrentThread.ManagedThreadId},是否为线程池线程:" +$"{Thread.CurrentThread.IsThreadPoolThread}";}}
}

运行后,如图所示:

95effbfa47141bd5fa40aa58d28abac3.png

从结果中我们可以看出,两种操作的方式在概念上是等同的,但是第二种方式中编译器隐式处理了异步相关的代码,背后的逻辑更为复杂,我们在后续小节中会借助示例再详细说明这些内容。

c1b08f1ba17f8034c5d17f242a05d94f.png

多个连续的await

我们已经得知了使用await的代码行将会异步执行,那么如果我们在同一个async方法中使用多个连续的await,它们会并行异步执行吗?我们不妨一试。

如图所示,我们依然定义TPL和Async函数进行对比:

e26bb39e3e0b12b98d22817df7b7c1c7.png

我们在定义AsyncAwait方法时,依然使用同步代码的方式进行书写,唯一的不同之处是连续使用了两个await声明。

而在TPL方法中,则使用了一个容器任务,来处理所有相互依赖的任务。然后启动主任务,并为其添加一系列的后续操作。当该任务完成时,会打印出其结果,然后再启动第二个任务,并抛出一个异常,打印出异常信息。

相关代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace asyncDemo
{class Program{static void Main(string[] args){Task t = AsyncTPL();t.Wait();t = AsyncAwait();t.Wait();Console.Read();}static Task AsyncTPL(){var continueTask = new Task(() =>{Task<string> t = GetInfoAsync("TPL1");t.ContinueWith(task =>{Console.WriteLine(t.Result);Task<string> t2 = GetInfoAsync("TPL2");t2.ContinueWith(innerTask =>Console.WriteLine(innerTask.Result),TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent);t2.ContinueWith(innerTask =>Console.WriteLine(innerTask.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);},TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent);t.ContinueWith(task =>Console.WriteLine(t.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.AttachedToParent);});continueTask.Start();return continueTask;}async static Task AsyncAwait(){try{string result = await GetInfoAsync("Async1");Console.WriteLine(result);result = await GetInfoAsync("Async2");Console.WriteLine(result);}catch (Exception ex){Console.WriteLine(ex);}}async static Task<string> GetInfoAsync(string name){Console.WriteLine($"{name} 开始执行!");await Task.Delay(TimeSpan.FromSeconds(2));if (name == "TPL2"){throw new Exception("发生异常!");}return $"{name}的线程Id为:{Thread.CurrentThread.ManagedThreadId},是否为线程池线程:" +$"{Thread.CurrentThread.IsThreadPoolThread}";}}
}

运行后,执行结果如图所示:

2eae22eec0153778bd7b49bfe88131fb.png

我们从结果中可以看出,TPL的后续依赖任务会按照我们的书写顺序依次执行,让人讶异的是await,它并没有并行执行,而也是顺序执行的。Async2任务只有等Async1任务完成后才会开始执行,但它为什么是异步程序呢?

事实上,它并不总是异步的,当使用await时,如果一个任务已经完成,我们会异步地得到相应的任务结果。否则,在看到await声明时,通常的行为是方法执行到await代码行应立即返回,且剩下的代码会在一个后续操作任务中执行。因此等待操作结果时,并没有阻塞程序执行,这是一个异步调用。当AsyncAwait方法中的代码在执行时,除了可以在Main中执行t.Wait外,我们可以执行其他任何任务。但主线程必须等待直到所有异步操作完成,否则主线程完成后会停止所有异步操作的后台线程。

这两段代码中,如果要比较TPL和await,那么则是TPL方法的书写更容易阅读和理解,调用层次更为清晰,请记住一点,异步并不总是意味着并行执行。

c6dfd83da1850fb0a6cfc367340482f0.png

并行执行的await

现在我们已经得知了,异步并不总是并行的,那么它能不能通过某种手段或方式进行并行操作呢?答案是可以的,我们一起看一下如何实现:

dcff796879accb294233c65dc39e333e.png

这里我们定义了2个不同的Task分别运行3秒和5秒,然后使用Task.WhenAll来创建另一个任务,该任务只有在所有底层任务完成后才会执行,之后我们等待所有任务的结果。

相关实现代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace asyncDemo
{class Program{static void Main(string[] args){Task t = AsyncProcessing();t.Wait();Console.Read();}async static Task AsyncProcessing(){Task<string> t1 = GetInfoAsync("任务1", 3);Task<string> t2 = GetInfoAsync("任务2", 5);string[] results = await Task.WhenAll(t1, t2);foreach (string result in results){Console.WriteLine(result);}}async static Task<string> GetInfoAsync(string name, int seconds){await Task.Delay(TimeSpan.FromSeconds(seconds));//await Task.Run(() => Thread.Sleep(TimeSpan.FromSeconds(seconds)));return $"{name}的线程Id为:{Thread.CurrentThread.ManagedThreadId},是否为线程池线程:" +$"{Thread.CurrentThread.IsThreadPoolThread}";}}
}

运行后,结果如图所示:

d205fe2e30e15d06c906405b57dafa49.png

根据程序运行的结果我们可以看到,5秒之后,我们获取到了所有的结果,说明这些任务是同时运行的。这里还有一个有趣的现象是,两个任务是被同一个线程池中的工作线程执行的,为什么会这样呢?这时候我们可以注释掉Task.Delay这行代码,并取消对Task.Run的注释,再次运行后,结果如图所示:

ddf34116d106ea03fff5af93ee5f7992.png

此时我们会发现,两个任务会被不同的工作线程执行。

造成这种情况的原因是Task.Delay在幕后使用了一个计时器,它的执行过程如下:

1、从线程池中获取工作线程,它将等待Task.Delay返回结果;

2、Task.Delay方法启动计时器,并指定一块代码,该代码会在计时器到了Task.Delay中指定的时间后进行调用,之后立即将工作线程返回线程池中;

3、当计时器事件运行时(类似于Timer类),我们会再次从线程池中获取一个可用的工作线程并运行计时器给它的代码(可能会是我们之前使用过的工作线程)。

而Task.Run方法则不同,它的执行过程如下:

1、从线程池中获取工作线程,并将其阻塞几秒钟;

2、获取第二个工作线程,也将其阻塞几秒钟。

在此过程中,两个工作线程并无法做其他事,只能进行等待操作,因此在某种程度上,这两个工作线程是被浪费掉了。

所以我们在实际使用时,尽量使用Task.Delay的方式进行并行操作,而不是使用Task.Run。

c68d8931bfa5b221ecf58a455ecc4c15.png

处理异常

在异步函数中,处理异常可以像同步代码那样使用try-catch去处理,但是在不同的场景下,也有不同的使用方式,下面我们一起来看看有哪些常见的使用场景,如图所示:

a3c9f6f07d481070448696311db63181.png

我们分别定义了三种场景:单个异常、多个异常及多个异常的异常集合。相关实现代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace asyncDemo
{class Program{static void Main(string[] args){Task t = AsyncProcessing();t.Wait();Console.Read();}async static Task AsyncProcessing(){Console.WriteLine("1、单个异常");try{string result = await GetInfoAsync("任务1", 2);Console.WriteLine(result);}catch (Exception ex){Console.WriteLine($"异常内容:{ex}");}Console.WriteLine("-----------------------------------------------------");Console.WriteLine("2、多个异常");Task<string> t1 = GetInfoAsync("任务1", 3);Task<string> t2 = GetInfoAsync("任务2", 2);try{string[] results = await Task.WhenAll(t1, t2);Console.WriteLine(results.Length);}catch (Exception ex){Console.WriteLine($"异常内容:{ex}");}Console.WriteLine("-----------------------------------------------------");Console.WriteLine("3、多个异常的异常集合");t1 = GetInfoAsync("任务1", 3);t2 = GetInfoAsync("任务2", 2);Task<string[]> t3 = Task.WhenAll(t1, t2);try{string[] results = await t3;Console.WriteLine(results.Length);}catch{var ae = t3.Exception.Flatten();var exceptions = ae.InnerExceptions;Console.WriteLine($"异常发生数量:{exceptions.Count}");}}async static Task<string> GetInfoAsync(string name, int seconds){await Task.Delay(TimeSpan.FromSeconds(seconds));throw new Exception($"异常来自于:{name}");}}
}

执行后的结果如图所示:

54bf37ec943bd3c9d4a5c34297f28d12.png

从执行结果我们可以看出,如果在可能发生多个异常的场景下,仍直接使用try-catch的方式处理异常,那么只能从底层的AggregateException中获取到第一个异常。

为了得到所有的异常信息,我们需要使用await任务的Exception属性。在第三种场景中,我们使用了AggregateException的Flatten方法,将层级异常放入一个列表,从而达到获取所有异常的效果,在实际使用时应多加注意。

71df4ef7849fcad7bc9a3ab18cc7fff5.png

小结

至此为止,关于异步函数的特性及使用方式就已经介绍完毕。通过异步模型的发展历程我们可以看出,为了应对不同时期的需求,异步模型也经历了由复杂到简单的过程。最终我们使用的异步函数模式,可以使得程序在编写代码时,能用编写同步代码的方式来实现异步,大大降低了复杂度,也提升了代码可读性。由于该思想和语法相当简洁,在其他语言中也借鉴了类似的语法,如JavaScript在ES6标准中也引入了async-await的写法来实现异步,避免了多个回调嵌套的尴尬方式。

但关于async-await本身,C#编译器在背后通过及其复杂的原理为我们屏蔽了底层的细节,包括为何不能使用async void等等,这些原理还是建议大家有时间的话进行一些挖掘和探究,学习背后的设计思想,会对我们的程序设计思维大有裨益。

.Net异步编程系列的文章,到此也暂时告一段落了。我个人在后面的日子中也会将主要精力投入到架构设计和微服务等前沿技术中,同时会总结一些个人的心得与体会形成其他系列的分享,请大家拭目以待。也感谢所有阅读此系列文章的读者,感谢大家的反馈,陪伴我度过一段难忘的时光,我们下一期再会!

185ff2340d4bc27a38389dbe57ead0b1.png

参考

  1. 1.避免 Async Void https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming

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

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

相关文章

NSInteger,NSUInteger,NSNumber

2019独角兽企业重金招聘Python工程师标准>>> Objective-C入门教程10:数字类型&#xff08;NSInteger,NSUInteger,NSNumber&#xff09; 柳志超博客 Program Objective-C Objective-C入门教程10:数字类型&#xff08;NSInteger,NSUInteger,NSNumber&#xff09; p…

听说麦当劳,买一个雪糕就送一个男友!

1 麦当劳买雪糕免费送男友&#xff01;▼2 当90后成了家长......▼3 不要跟有鼻子的人握手可能刚刚扣过鼻屎▼4 鸟&#xff1a;别瞎玩&#xff01;快开车&#xff01;▼5 凭实力当上群主&#xff01;▼6 要是有喜欢的女生千万不要问她闺蜜的意见▼7 终于&#xff0c;我们…

windows server 2008更新补丁失败排错

首先描述故障故障&#xff1a;1&#xff0c;windows服务器上丢失了共享磁盘。2&#xff0c;打开服务器管理器报错3&#xff0c;更新补丁报错&#xff0c;错误代码&#xff1a;800B01004&#xff0c;手动安装.net 3.5安装包同样报错话说这次出差帮客户解决问题。遇到了这样一个错…

WPF实现统计图

WPF开发者QQ群&#xff1a; 340500857 | 微信群 -> 进入公众号主页 加入组织有小伙伴提出需要实现统计图。 由于在WPF中没有现成的统计图控件&#xff0c;所以我们自己实现一个。PS&#xff1a;有更好的方式欢迎推荐。01—代码如下一、创建 Basi…

手绘图解:从零维到十维空间

全世界只有3.14 % 的人关注了爆炸吧知识事情是这样的&#xff0c;这周我给学生讲3dmax的课。为了让学生了解三视图我就顺便科普了一下什么是零维、一维、二维、三维空间。讲完不过瘾&#xff0c;感觉一支粉笔一块黑板讲维度是一件很爽的事情&#xff0c;那么.........接下来请同…

ISA server的常见身份验证方式

ISA 2006的几种常用验证方式&#xff1a;1. 基本验证&#xff1a;此验证方式不会被加密&#xff0c;只是以明文的方式来传递信息&#xff0c;不安全。如果在“网络”的“内部”属性中将“域”选项卡里边的“选择域”来配置默认域&#xff0c;那么就会把用户送来的帐户与密码信息…

字节前端终于开源!吹爆!

Semi Design 发布&#xff0c;前端同学的福音大家好&#xff0c;我是鱼皮。最近&#xff0c;字节跳动的抖音前端技术团队开源了一款企业级应用设计系统 Semi Design 。这也是他们团队在 GitHub 上首次公开的项目&#xff0c;短短几天&#xff0c;就收获了 3.6 k 个 star。GitH…

CSS2-3常见的demo列子总结

CSS2-3常见的demo列子总结 阅读目录 1. css超过一行或者多行后显示省略号。2. css图片未知高度垂直居中完美解决方案。3. 学习使用 :before和 :after伪元素回到顶部1. css超过一行或者多行后显示省略号。 Css实现超过一行后显示省略号&#xff1b;代码如下&#xff1a;<p st…

18张难以置信的照片,封面这张你就没见过

全世界只有3.14 % 的人关注了爆炸吧知识感谢网络&#xff0c;只要点几下鼠标&#xff0c;就能看到我们以前从未见过的东西——有些甚至是难以置信的&#xff01;鲸鱼的心脏水中的鲨鱼卵幼年的箭鱼萌萌哒世界上最高的棕榈树&#xff0c;简直以为是PS的没见过的话&#xff0c;很容…

.NET 生态系统的蜕变之 .NET 6

.NET 6 是自.NET 4 框架以来生态系统看到的最大版本更新&#xff0c;虽然.NET Core 是2014年开始非常大的一项重大战略举措&#xff0c;但是.NET 6是真正的具有强大动力的非常重要的版本。2021年11月9日即将正式发布的.NET 6, 也许你认为.NET 5才刚刚发布&#xff0c;我才刚开始…

我看你还能坚持多久?!

1 我看你还能坚持多久&#xff01;▼2 依旧是熟悉的配方▼3 到哪儿都不愁工作......▼4 请问&#xff0c;当事喵作何感想&#xff1f;▼5 池塘危险&#xff0c;请勿靠近&#xff01;&#xff08;图源网络&#xff0c;侵删&#xff09;▼6 望周知&#xff01;▼7 实在是无…

Hello Blazor:(13)查找HTML元素对应.razor文件

前言Blazor是基于组件的开发&#xff0c;每个组件都是以一个.razor文件形式存在。当应用程序变得越来越大并且.razor文件的数量和层次结构越来越多时&#xff0c;想很快弄清页面上的HTML元素是由哪个组件生成的&#xff0c;就变得不那么容易了&#xff01;FindRazorSourceFile介…

C++STL之string (转)

在学习cSTL中的string&#xff0c;在这里做个笔记&#xff0c;以供自己以后翻阅和初学者参考。 1&#xff1a;string对象的定义和初始化以及读写 string s1; 默认构造函数&#xff0c;s1为空串 string s2(s1); 将s2初始化为s1的一个副本 string s3("valuee");…

当年的毒王熊猫烧香,现在怎么样了?

全世界只有3.14 % 的人关注了爆炸吧知识放假&#xff0c;小编来到了远在73公里之外的天后宫&#xff0c;终于是了了本命年的一桩心事。回想上一个本命年&#xff0c;当时小编还是沉迷扫雷和蜘蛛纸牌的孩子...但当时却发生了一件令我很不爽的事——“熊猫烧香”席卷全国&#xf…

$query php,phpQuery让php处理html代码像jQuery一样方便

简介如何在php中方便地解析html代码&#xff0c;估计是每个phper都会遇到的问题。用phpQuery就可以让php处理html代码像jQuery一样方便。DEMO我下的是onefile版&#xff1a;phpQuery-0.9.5.386-onefile.zip然后在项目中引用。html文件test.html&#xff1a;Spiderman City Driv…

那个成人总会遇到的小问题……

结语超模君就问问&#xff1a;我还有机会10万&#xff0b;吗&#xff1f;&#xff08;溜了溜了&#xff09;莱布尼茨德国数学家莱布尼茨&#xff0c;被后人誉为“百科全书式的天才”&#xff0c;他的研究涉及逻辑学、力学等40多个领域。他创建了数学理论&#xff1a;微积分学。…

java继承接口和泛型,JavaSE习题 继承接口和泛型

问答题&#xff1a;1.子类在什么情况下可以继承父类友好成员&#xff1f;答&#xff1a;在同一个包内2.子类通过怎样的方法可以隐藏继承的成员变量&#xff1f;答&#xff1a;声明一个与父类相同变量名的成员变量3.子类重写继承的方法原则是什么&#xff1f;答&#xff1a;保证…

Github CodeSpaces 使用及定制化

Github CodeSpaces 使用及定制化IntroGithub 最近推出了很多令人兴奋的新功能&#xff0c;最近使用了 Github CodeSpaces&#xff0c;觉得还是挺不错的&#xff0c;CodeSpaces 相当于自己有了一个云主机&#xff0c;真正实现了云端开发&#xff0c;CodeSpaces 和 Github 做了很…

nat+端口转发,使得宿主机secureCRT可以访问vbox里linux虚拟机

为什么80%的码农都做不了架构师&#xff1f;>>> 环境&#xff1a;vbox或者叫vitrualbox连接虚拟机&#xff0c;由于公司内网不能分配IP&#xff08;不知道是不是这个原因&#xff09;&#xff0c;虚拟机用桥接得不到IP&#xff0c;没法实现虚拟机和宿主互相访问&am…

男人可以有多敷衍?

1 现在更流行「红茶女生」&#xff1f;&#xff08;via.白头叔&#xff09;▼2 谢谢&#xff0c;有被冒犯到&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼3 天使与恶魔▼4 男朋友可以多敷衍&#xff1f;▼5 你的高原红&#xff0c;像极了妈妈的巴掌印&#xff…