C#异步编程几点需要注意的地方

 更新时间:2020年6月25日 10:34  点击:2174

尽量不要编写返回值类型为void的异步方法

在通常情况下,建议大家不要编写那种返回值类型为void的异步方法,因为这样做会破坏该方法的启动者与方法本身之间的约定,这套约定本来可以确保主调方能够捕获到异步方法所发生的异常。

正常的异步方法是通过它返回的Task对象来汇报异常的。如果执行过程中发生了异常,那么Task对象就进入了faulted(故障)状态。主调方在对异步方法所返回的Task对象做await操作时,该对象若已处在faulted状态,系统则会将执行异步方法的过程中所发生的异常抛出,反之,若Task尚未执行到抛出异常的那个地方,则主调方的执行进度会暂停在await语句这里,等系统稍后安排某个线程继续执行该语句下方的那些代码时,异常才会抛出。

总结一句话就是:void的异步方法发生异常时,开发者得不到任何通知,程序既不会触发普通的异常处理程序,也不会把这些异常记录下来。总之,这会让相关的线程默默的终止掉。

不要把同步方法与异步方法组合起来使用

用async关键字来修饰的方法意味着该方法有可能会在执行完所有工作之前就把控制权返回给主调方,而且,它返回给主调方的是个代表工作进度的Task对象。主调方可以查询此对象的状态,以了解该工作是否已经完成、尚未完成还是在执行过程中发生了故障。此外,这种方法还在暗示主调方:本方法所执行的工作可能要花费很长时间,因此建议你先去做其他一些事情,稍后再来向我索要结果。

与此相反,如果把某个方法设计成同步方法,那么意味着当该方法执行完毕时,它的后置条件必定能够得到满足。无论这个方法要花多长时间去完成工作,它都会采用与主调方相同的资源来完成,主调方必须等这个方法彻底执行完毕才能向下执行。

这两种方法单独写起来都很清晰,但是如果把他们组合在一起就会让方法变得十分难用,而且有可能导致各种bug,如死锁。因此,这里提出两条重要的原则。第一,不要让同步方法必须等待异步方法执行完毕才能往下执行(尽量不用Wait()以及.result这些阻塞式的方法)。第二,不要让异步方法把虽然耗时很长、计算量很大但是完全可以由自己执行的工作转交给另一个异步任务去做。'

当然对于第二点,这并不是说计算量较大的任务绝对不能放在单独的线程中执行,而是说不应该把只用一个线程就能迅速做好的任务刻意的拆解成许多个较小的部分,并把他们分别放在多个新的线程上执行,而是应该把整个任务都交给某个线程来执行才对。

使用异步方法时应尽量避免线程分配

异步任务看上去好像很神奇,因为这种任务刻意转移到另一个地方去做,使得开启这项任务的异步方法可以在该任务完成之后,从早前暂停的地方继续往下推进。不过,要想发挥异步任务的功效,就必须保证把这项任务交出去确实能够少占用一些资源,而不是仅仅会在相似的资源之间进行上下文切换。

如:对于一个控制台程序,如果只是执行一项计算量较大且耗时较长的任务(或者说,运行时间较长的CPU密集型的任务),那么把该任务单独放在另一个线程中并没有多大好处。因为这样做只能让工作线程始终处于繁忙状态,而主线程则必须一直卡在那里等待工作线程把任务做完。在这种情况下,实际上是用两个线程来完成原本只需要一个线程就能做好的工作,造成了资源的浪费。

避免不必要的上下文切换

目前C#代码中使用async以及await实现的异步方法默认是把await之后的代码放在早前捕获的那个上下文中执行的,这是因为这样做比较稳妥,它最多只会引发几次无谓的上下文切换,而不会使程序出现重大的错误,与之相反,如果系统不把山下文切换回去,那么万一遇到的是只能在特定的上下文中才能执行的代码,那么程序就有可能崩溃。因此,无论有没有必要切换上下文,系统都会切换至早前捕获到的那个上下文,并把await之后的语句放在那个上下文执行。

如果不想让系统做出这样的安排,那么可以调用ConfigureAwait()方法。这表示接下来的那些代码无须放在早前捕获的上下文中执行。例如在很多程序集中,await语句之后的那些代码一般都与上下文无关,因此与,可以调用Task对象的ConfigureAwait()方法告诉系统,在执行完这项任务之后,不必专门把await下面的代码放在早前捕获的上下文中运行。如下所示:

public static async Task<XElement> ReadPacket(string url)
{
	var result=await DownloadAsync(url)
				.ConfigureAwait(false);
	return XElement.Parse(result);			
}

C#语言默认让程序把await下面的语句都放在早前捕获的上下文中执行,这样做虽然较为安全,但是会降低程序的效率。因此为了让用户能够更加顺畅的使用程序,我们应该调整代码的结构,把必须运行在特定上下文的代码剥离出来,并尽量考虑在await语句那里调用ConfigureAwait(false),使得程序可以把语句下面的代码放在默认上下文中运行,而不是切换回早前的上下文。

通过Task对象来进行异步开发

Task(任务)是一种抽象机制,可以用来表示某项工作,于是,就能够把该工作转交给其他资源去完成。Task类型以及与之相关的类与结构体提供了丰富的API,让开发者可以操控Task对象以及由该对象所表示的工作。此外,Task对象自身也具备一些方法与属性,可以用来操作本对象所表示的任务。这些Task对象可以合起来构成一项比较大的任务,他们之间既能够按照顺序执行,也能够平行的执行。

可以通过await语句来确保某些任务之间能够按照一定的顺序执行,也就是说,只有当该语句所要等待的那项工作完毕之后,语句下方的代码才能够执行。

总之,由于C#提供了一套丰富的API,因此可以写出相当优雅的算法来处理Task对象,并对这些对象所表示的任务进行安排。对任务的用法理解的越透彻,写出来的异步代码越清晰。

这里简单说明两个常用的API:

WhenAll:会根据现有的一批任务创建出一项新的任务,只有当那批任务全部执行完毕时,这项新人物才能够完成。对Task.WhenAll所返回的新任务进行await操作会获得一份列表,早前的那些任务的执行结果就位于该列表中。

WhenAny:为了尽早的获得某个结果,可能启动多项任务,使得他们分别从不同的途径去获取该结果。只要其中有一项任务完成,你的目标就达成了,针对这项需求,可以考虑使用Task.WhenAny方法,并把自己所创建的那批任务传进去。对WhenAny方法所返回的Task对象进行await操作可以获取到一项任务,它指的就是这批任务中最先执行完毕的那项任务。

考虑实现任务的取消协议

异步任务的编程模型(也叫基于任务的异步编程模型)提供了标准的API,用来取消任务或者广播任务的执行进度。虽然这些API是可选的,但如果某项任务确实能够汇报其进度,或者能够予以取消,那就可以考虑用合适的办法来实现这些API。

针对需要取消的任务,我们可以通过CanclelationTokenSource对象来进行取消操作。这种对象是一种起到中介作用的对象。该对象处在有可能发出取消请求的客户代码与支持取消功能的那项操作之间。

如果正在执行的任务发现客户端想要取消该操作,那么它就会通过ThrowIfCanclellationRequested()方法抛出TaskCanclledException异常,庸医表示整个工作流程没有能够完全得到执行。

此外,返回值类型为void类型的异步方法不应该支持取消功能。

缓存泛型异步方法的返回值

可能你在进行异步编程的时候对异步方法设置的返回类型都是Task或者Task<T>,然而有些时候把返回值类型设为Task可能会影响性能。如果某个循环或某段代码需要频繁的运行,那么系统就有可能分配很多个Task对象,从而占用相当多的资源。好在C#提供了一种新的类型,叫做ValueTask<T>对象,他用起来比普通的Task更为高效。该类型是值类型,因此创建这种类型的对象时,不需要再分配额外的空间。这个好处使得我们可以多创建一些这样的对象,而不用担心它会像Task对象那样占据过多的资源。如果你的异步方法可以根据早前缓存起来的结果直接返回相应的值,那么尤其应该考虑把返回值类型设置为ValueTask<T>。

其次,ValueTask提供了一个能够接受Task参数的构造函数,这个构造函数会在其内部等候该Task的执行结果。

总结

今天分享的内容比较多,而且很多都比较难理解,不过确实是写出高性能异步方法所必须要掌握的技巧。由于时间较短,因此也没来得及通过代码进行讲述,所以需要有一定的基础才能看懂,不过还是希望对您有所帮助。

以上就是C#异步编程几点需要注意的地方的详细内容,更多关于C#异步编程的资料请关注猪先飞其它相关文章!

[!--infotagslink--]

相关文章

  • C#实现简单的登录界面

    我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
  • 浅谈C# 字段和属性

    这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
  • C#中截取字符串的的基本方法详解

    这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
  • C#实现简单的Http请求实例

    这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
  • C#连接SQL数据库和查询数据功能的操作技巧

    本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
  • C#中new的几种用法详解

    本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
  • 使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)

    这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#开发Windows窗体应用程序的简单操作步骤

    这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
  • C#从数据库读取图片并保存的两种方法

    这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
  • C#和JavaScript实现交互的方法

    最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
  • C++调用C#的DLL程序实现方法

    本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
  • 轻松学习C#的基础入门

    轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
  • C#变量命名规则小结

    本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
  • C#绘制曲线图的方法

    这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • C# 中如何取绝对值函数

    本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
  • c#自带缓存使用方法 c#移除清理缓存

    这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
  • c#中(&&,||)与(&,|)的区别详解

    这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
  • 经典实例讲解C#递归算法

    这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
  • C#学习笔记- 随机函数Random()的用法详解

    下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
  • C#中list用法实例

    这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25