浅谈.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…

作业优先调度java代码_如何在触发它时在java代码中为hadoop作业设置优先级?

这是我的工作人员的外表。如何在触发这项工作时设定低优先级&#xff1f;public int run(String[] args) throws Exception {this.initJob();Path outputPath new Path(args[2]);FileInputFormat.setInputPaths(job, args[0]);FileOutputFormat.setOutputPath(job, outputPath…

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

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

07公务员考试公共基础知识模拟试卷1及答案--A

本试题分两部分&#xff0c;第一部分为客观试题&#xff0c;第二部分为主观试题&#xff0c;客观试题60分&#xff0c;主观试题40分&#xff0c;满分100分。考试时间180分钟。第一部分客观试题 一 单项选择题 1&#xff0e;2006年12月29&#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…

java guava json文件_Json解析文件位置的问题

有这么一段Json{"name": "java3d:vecmath:1.3.1"},{"name": "net.sf.trove4j:trove4j:3.0.3"},{"name": "com.ibm.icu:icu4j-core-mojang:51.2"},{"name": "net.sf.jopt-simple:jopt-simple:4.5&q…

在此之前的软件系统做开发—需求的研究框架

最近的研究需要做一个软件&#xff0c;从一些方面中学习了下面的。当然&#xff0c;这些都是需求只是初步框架。也创下了基本的设计和开发&#xff0c;只是让软件公司可能对软件整体的了解需求和促进软件有一个粗略的估计。公司 实现的根本目的 现有的软件的情况下&#xff08;…

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

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

[for循环之等腰三角形]

public class IsoscelesTriangle { /*** 等腰三角形*/public void print1(){//形如 ▲向上的 for(int i1;i<5;i){for(int k1;k<5-i;k)System.out.print(" ");for(int j1;j<2*i-1;j)System.out.print("*");System.out.print("\n");}}pub…

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;很容…

Java 关于中文乱码处理的经验总结

为什么说乱码是中国程序员无法避免的话题呢&#xff1f;这个首先要从编码机制上说起&#xff0c;大家都是中文和英文的编码格式不是一样&#xff0c;解码也是不一样的&#xff01;如果中国的程序员不会遇到乱码&#xff0c;那么只有使用汉语编程。汉语编程是怎么回事我也不大清…

[信息收集] HCOMP 2010概况及收录论文

这是第二届HCOMP. 全称&#xff1a;Human Computation Workshop(HCOMP2010) 时间&#xff1a;July 25, 2010 地点&#xff1a;Washington, D.C., USA 收录论文情况&#xff1a;共有4个session&#xff0c; 包括&#xff1a; Invited Talk(1篇&#xff09;, Market design(3篇),…

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

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

C语言程序读写文件(文件内存一个十进制数,每读一次数值加一)

1.问题&#xff1a;C语言程序实现读写一个txt文件&#xff0c;txt文件中存储一个十进制数、每读一次该数值加一。 2.实现&#xff1a;新建一个文件夹&#xff0c;在该文件夹中建一个outputFileName.txt文件、内容是&#xff1a;1&#xff0c;再在该文件夹中新建一个t.c文件、内…

php 电梯程序设计,教你写出京东电梯式轮播

效果知识点&#xff1a;企业布局技巧&#xff0c;如何高效的编写CSS样式&#xff0c;常用选择器&#xff0c;基本标签&#xff0c;盒子模型&#xff0c;jquery类库调用&#xff0c;JS特效编写&#xff0c;JS编程思维等。京东电梯式轮播源码&#xff1a;Document*{margin:0px;}/…

我看你还能坚持多久?!

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