C#并发编程之异步编程(二)

写在前面

前面一篇文章介绍了异步编程的基本内容,同时也简要说明了async和await的一些用法。本篇文章将对async和await这两个关键字进行深刻探讨,研究其中的运行机制,实现编码效率与运行效率的提高。html


异步方法描述:使用async修饰符来标识一个方法或Lambda表达式的,被称之为异步方法。数据库

异步方法编译:编译器在遇到await表达式后会截断方法,并将剩余的异步方法注册为在等待任务完成后须要继续执行的后续部分。编程

异步方法基础及其运行流程

Async和Await

异步方法使用async修饰,该方法包含一个或多个await表达式或语句,方法同步运行,直至到达第一个 Await,此时暂停,直到等待的任务完成,在任务完成后,控制权返回给方法的调用方。若是方法中并不包含await,则该方法不会像同步方法同样被挂起。网络

异步方法一般包含await运算符的一个或多个实例,但缺乏await表达式也不会致使生成编译器错误,之会由于没有await而发出警告,但编译依然经过。多线程

异步方法使用await关键字来肯定等待位置,但await表达式并不阻止正在执行到此位置的线程,也就是说异步方法在await表达式执行时只是暂停,并不会致使方法退出,只会致使finally代码块不运行。异步方法只有在等待的任务完成后,才能经过该位置并继续执行剩下的逻辑,控制权也在此处返回给异步方法的调用方。异步

若是异步方法未使用Await运算符标记暂停点,那么异步方法会做为同步方法执行,即便有Async修饰符,也不例外。如如下示例async

   1:  public async static Task<string> GetUserInfoAsync()
   2:  {
   3:      User user = await db.User.FirstOrDefaultAsync();//此处会挂起
   4:   
   5:      Task<User> user = db.User.FirstOrDefaultAsync();//此处不会挂起,注意此处,返回值也变了,接下来会讨论一下异步方法的返回值
   6:   
   7:      return string.Empty;
   8:  }
 
具MSDN描述,aysnc关键字是一个非保留的关键字。 在修饰方法或 lambda 表达式时,它是关键字,await也做为关键字存在。 在全部其余上下文中,async和await都会将其解释为标识符。不过开发人员能够不用太过关注这段,只须要知道aysnc会将一个方法标识成异步方法,而await能够挂起异步方法的执行便可。

关键点ide

一、和被async修饰的方法不同,若是方法中含有await关键字,方法必须使用async标识符,不然编译不经过。异步编程

二、在异步编程过程当中,比较推荐的作法是,被标记了async关键字的异步方法应该包含至少一个await表达式或语句。性能

三、异步方法的命名以Async结尾 

异步返回类型和异常处理

须要说明的是,本文所讨论的异步方法指的是基于任务的异步编程模型,返回值是,Task或Task<TResult>。

一、若是方法须要返回string类型,那么将返回Task<string>。若是方法没有指定返回类型,那么将返回Task。每一个返回的任务都表示正在进行的工做,任务封装有关异步进程状态的信息,若是未成功,则会引起异常。异步方法返回 Task 或 Task<TResult>。 返回任务的属性携带有关其状态和历史记录的信息,如任务是否完成、异步方法是否致使异常或已取消以及最终结果是什么。 可以使用await运算符访问这些属性。

 
   1:  public async static Task<User> GetUserInfoAsync()
   2:  {
   3:      User user = await db.User.FirstOrDefautAsync();
   4:   
   5:      return user;
   6:  }

二、若是等待的任务返回异步方法致使异常,则 await 运算符会以同步方式抛出异常。若是等待的返回任务的异步方法取消,await运算符引起OperationCanceledException。若是异步方法中没有使用await阻塞,可使用try-catch捕捉异常,只是异常发生的时机可能会滞后。

异步方法的运行流程

了解异步方法的运行机制,就是要了解异步编程中的控制流是如何一步步执行的。若是须要详细了解控制流,能够异步到MSDN中查看。

下图及其描述摘自MSDN

navigation-trace-async-program

关系图中的数值对应于如下步骤。

  1. 事件处理程序调用并等待 AccessTheWebAsync 异步方法。

  2. AccessTheWebAsync 建立HttpClient实例并调用GetStringAsync异步方法,获取的内容字符串方式返回。

  3. GetStringAsync 中发生了某种状况,该状况挂起了它的进程。 可能必须等待其余阻止任务完成。 为避免阻止资源,GetStringAsync 会将控制权出让给其调用方 AccessTheWebAsyncGetStringAsync 返回Task<TResult>,其中 TResult 为字符串,而且 AccessTheWebAsync 将任务分配给 getStringTask 变量。 该任务将调用GetStringAsync正在进行的进程,在调用完成时产生返回字符串给urlcontent。

  4. 因为还没有等待 getStringTask,所以,AccessTheWebAsync 能够继续执行而不依赖于 GetStringAsync 最终结果的完成。 该任务继续调用同步方法 DoIndependentWork

  5. DoIndependentWork 做为一个同步方法,在自身工做完成后返回到调用方。

  6. AccessTheWebAsync 已运行完毕,能够不受 getStringTask 的结果影响。 接下来,AccessTheWebAsync 须要计算并返回已下载的字符串的长度,但该方法只有在得到字符串的状况下才能计算该值。

    所以,AccessTheWebAsync 使用一个 await 运算符来挂起其任务,并把控制权交给调用 AccessTheWebAsync 的事件处理程序。 AccessTheWebAsyncTask<int>返回给调用方。 该任务将计算下载字符串长度。 

  7. GetStringAsync 完成并生成一个字符串结果。 字符串结果不是经过按你预期的方式调用 GetStringAsync 所返回的。 (记住,该方法已返回步骤 3 中的一个任务)。相反,字符串结果存储在表示 getStringTask 方法完成的任务中。 await 运算符从 getStringTask 中检索结果。 赋值语句将检索到的结果赋给 urlContents

  8. AccessTheWebAsync 获取字符串结果时,该方法能够计算字符串长度。 而后,AccessTheWebAsync 工做也将完成,而且等待事件处理程序的继续使用。 事件处理程序也将最终得到字符串的长度信息。

注意:

若是 GetStringAsync(所以 getStringTask)在 AccessTheWebAsync 等待前完成,则控制权会保留在 AccessTheWebAsync中。 若是异步调用过程 (AccessTheWebAsync) 已完成,而且 AccessTheWebSync 没必要等待最终结果,则挂起而后返回到 getStringTask 将形成资源浪费。

在调用方内部(此示例中的事件处理程序),处理模式将继续。 在等待结果前,调用方能够开展不依赖于 AccessTheWebAsync 结果的其余工做,不然就需等待片刻。 事件处理程序等待 AccessTheWebAsync,而 AccessTheWebAsync 等待 GetStringAsync。

异步编程对性能的影响

 在.NET异步编程中,async和await不会建立其余线程,同时异步方法不会在其自身线程上运行,所以它不须要多线程。只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。可使用Task.Run将占用大量CPU的工做移到后台线程,可是后台线程不会帮助正在等待结果的进程变为可用状态。

对于异步编程而言,基于异步的方法优于几乎每一个用例中的现有方法。具体而言,这种方法优于BackgroundWorker的I/O绑定操做由于代码更简单且无需防止争用条件。结合Task.Run使用时,异步编程比BackgroundWorker更适用于CPU绑定的操做,由于异步编程将运行代码的协调细节与Task.Run传输至线程池的工做区分开来。

那么异步编程对线程的影响又是什么呢,相比你们应该都知道,ASP.NET中有两类线程,工做线程,和IO线程。

其中工做线程处理普通请求的线程,也是咱们用得最多的线程。这个线程是有限的,是根CPU的个数相关的。IO线程,好比与文件读写,网络操做等是能够异步实现而且使性能提高的地方。I/O线程一般状况下是空闲的。因此可使用IO线程来代替工做线程,一方面充分运用了系统资源,另外一方面也节省了工做线程调度及切换所带来的损耗。

由此咱们须要明白,在I/O密集型处理时,使用异步能够带来很大的提高,好比数据库操做以及网络操做。

即使异步编程带来性能的提高,可是运用不慎,也会对系统性能产生副作用,好比直接使用Task.Run或者Task.Factory.StartNew所带来的异步编程,这些方式会占用工做线程以及工做线程之间的切换。 

异步编程须要注意的地方


一、同时async和await侵入性或者传递性很强,全部调用的地方都须要同步使用async和await,这对系统中老代码的修改产生了很大的影响。

二、异步编程中没法使用lock锁,由于异步方法不会在自身线程上运行,lock就变成了多余的了。但异步编程场景下可使用AsyncLock锁,对相应的代码进行锁定。

三、异步编程里,比较推荐的作法是避免上线文延续,此处再也不作更多说明,参考个人前一篇文章《异步编程(一)》

四、异步编程是否真的提高了系统性能,目前来看大多数场景下是提高了,尤为在I/O操做比较密集的业务场景下,好比查询数据库和网络调用。

相关文章
相关标签/搜索