正确使用异步操做

本想写一点有关LINQ to SQL异步调用的话题,可是在这以前我想仍是先写一篇文章来阐述一下使用异步操做的一些原则,避免有些朋友误用致使程序性能反而下降。这篇文章会讨论一下在.NET中有关异步操做话题,从理论出发结合实际,以澄清概念及避免误用为目标,而且最后提出常见的异步操做场景和使用案例。这样咱们就能够知道何时该使用异步操做,何时会得不偿失。数据库

那么咱们先来确认一个概念,那就是“线程”。请注意,若是没有特殊说明,本文中出现的“线程”所指的是CLR线程池(Thread Pool)中的托管线程,它和Windows线程或纤程(fiber)并非同一个的概念。一样,它也不是指System.Thread类的实例。简单地说,它是由CLR管理的工做执行单元,每当须要执行任务时,CLR就会分配一个这样的执行单元去工做。当全部的线程池内的线程都用完以后就没法执行新的任务了,一个托管线程在任务完成以后被释放为止。线程池自己是一个“对象池”,会在须要新对象(托管线程)时建立,而在对象不须要以后(一段特定时间以内没有新任务须要分配托管线程)负责销毁以释放资源。至于线程池的线程数量,在CLR 2.0 SP1以前的版本中是CPU数 * 25,不过从CLR 2.0 SP1以后就变成了CPU数 * 250。不过无论怎么样,线程池内的线程是有限的,咱们必须合理地使用它。编程

之前的计算机只有一个CPU,理论上同一时刻只能执行一个任务。而现在的超线程、多核、甚至是真正的多个CPU都使计算机可以同时运行多个任务。多线程编程的一个重要特色就是可以充分利用CPU的运算能力,更快地完成某个任务。很明显,若是一个很是庞大的计算任务只交由一个线程来完成,那么只能让一个CPU参与运算。可是若是将一个大任务拆分红多个互不影响的子任务,那么就能让多个CPU同时参与运算,所花的时间天然就少了。若是某个操做的目的是进行大量运算,或者说须要花费大量时间运算上的操做,咱们将其称做“Compute-Bound Operation”,也就是受运算能力限制的操做。服务器

与“Compute-Bound Operation”相对的则是“IO-Bound Operation”。“IO-Bound Operation”是指那些因为受到外部条件限制,完成这样一个任务须要在IO上花费大量时间的操做。例如读取一个文件,或者请求网络上的某个资源。对于这种操做,计算的线程再多,运算能力再强也无济于事,由于任务受到的是硬盘、网络等IO设备带来的限制。对于IO-Bound Operation,咱们能作的只有“等待”。网络

对于“同步操做”来讲,“等待”就意味着“阻塞”,一个线程将会“无所事事”直至操做完成。这种作法在许多时候会带来各类问题,所以就出现了“异步操做”,可是一样是“异步操做”,不一样的任务,不一样的状况,它解决问题的方式和带来的效果也是不一样的。我下面就经过生活中的实例来讲明这些内容:多线程

老赵的朋友开了一家餐馆,请了10个工做人员。最近那个朋友常常向老赵抱怨,说工做人员人手老是不够,在客人比较多的时候,老是来不及招呼他们。老赵一问才得知,这家餐馆的工做方式比较特别:当客人来用餐时,就会有工做人员迎上去热情招待,当客人点好菜以后,工做人员就会去进入厨房亲自下厨——没错,就是这样——作完以后,工做人员会将饭菜端至客人面前,而后就去招待别的客人。由于烧菜每每须要很长时间,所以在某些时候就会发现全部的工做人员都在厨房,可是却没有人点菜。因而老赵给朋友出了个主意:让几个工做人员做为服务员,只负责招呼客人,剩下的就当厨师,一直在厨房工做。当客人点菜以后,服务员就把客人的需求告诉厨师,厨师开始工做,而服务员就能够去招呼其余客人了。朋友顿悟,问题就这样迎刃而解了。异步

固然,上面故事中老赵的朋友实在太笨,现实生活中的餐馆老板都不会犯这种人员调度上的低级失误。开发一个客户端应用程序所遇到的状况每每就和以上的状况相似。在运行程序时,UI线程(服务员)负责显示界面(招待客人),当用户操做应用程序(点菜)以后,UI线程可使用同步操做进行运算(服务员亲自下厨),可是若是这是个长时间的Compute-Bound Operation(烧菜是个花费人手时间较长的操做),界面就没法重绘或响应用户请求了(没法招待客人了),这样的应用程序用户体验天然很差(客人以为服务质量低下)。可是只要UI线程使用异步操做(通知厨师),让另外一个线程(另外一个工做人员)来进行运算,UI线程就能够继续负责界面重绘或者其余用户操做(招待其余客人)了。性能

在这种的状况下,异步操做并无提升运算能力或者节省资源(仍是须要一我的员的工做),可是提供了较好的用户体验。不过咱们这时该怎么利用异步操做呢?在实际开发中,咱们可使用委托的BeginInvoke进行异步调用。操作系统

下面的例子则对应了另外一种状况:线程

老赵的那个开餐馆的朋友在小赚一笔以后准备再开一家快餐店。快餐店和餐馆有个不一样之处,那就是快餐店的食品生产了大都有机器完成。惋惜在这种状况下那个朋友仍是遇到了问题:机器数量绰绰有余,可是人手仍是不够。原来如今的作法仍是至关不科学:服务员知道客人须要的食品以后,就将原料塞入机器,并看着机器是如何将原料变为美味的。当机器的工做完成以后,服务员便将食品打包并送出,而后继续招待别的客人。老赵听后仍是啼笑皆非:为啥服务员不能在机器工做的时候就去招待别的客人呢?code

  与这个示例对应的能够是一个ASP.NET应用程序。在ASP.NET中每一个请求(客人)都会使用一个线程池内的线程(服务员)来处理(招待),处理中极可能须要访问数据库(使用机器),对于普通的作法,处理线程会等待数据库操做返回(服务员看着机器直至完成)。对于Web服务器来讲,这极可能是个长时间的IO-Bound Operation,若是线程长时间被阻塞极可能就会下降Web应用程序的性能,由于线程池里的线程用完以后(服务员都去看炉子了),就没法处理新的请求了(没人招待客人了)。若是咱们可以在数据库进行长时间查询操做时,让线程去处理其余的请求(招待其余客人)。这样,咱们只须要在数据库操做完成以后继续处理(打包)并将数据发送给客户端(送出)便可。

这就是处理IO-Bound Operation的方式,很显然,这也是一个异步操做。当咱们但愿进行一个异步的IO-Bound Operation时,CLR会(经过Windows API)发出一个IRP(I/O Request Packet)。当设备准备稳当,就会找出一个它“最想处理”的IRP(例如一个读取离当前磁头最近的数据的请求)并进行处理,处理完毕后设备将会(经过Windows)交还一个表示工做完成的IRP。CLR会为每一个进程建立一个IOCP(I/O Completion Port)并和Windows操做系统一块儿维护。IOCP中一旦被放入表示完成的IRP以后(经过内部的ThreadPool.BindHandle完成),CLR就会尽快分配一个可用的线程用于继续接下去的任务。

这种作法的须要一个重要条件,这就是发出用于请求的IRP的操做可以当即返回,而且这个IO操做不会使用任何线程。而此时,这种异步调用是真正地在节省资源,由于咱们能够腾出线程用来处理其余任务了,这就是和第一种异步调用的最大区别。不过很惋惜,这种作法显然须要操做系统和设备的支持,也就是只有特定的操做才能享受这些待遇。那么.NET Framework中哪些操做能从中获利呢?

  • FileStream操做:BeginRead、BeginWrite。调用BeginRead/BeginWrite时会发起一个异步操做,可是只有在建立FileStream时传入FileOptions.Asynchronous参数才能获取真正的IOCP支持,不然BeginXXX方法将会使用默认定义在Stream基类上的实现。Stream基类中BeginXXX方法会使用委托的BeginInvoke方法来发起异步调用——这会使用一个额外的线程来执行任务。虽然当前调用线程当即返回了,可是数据的读取或写入操做依旧占用着另外一个线程(IOCP支持的异步操做时不须要线程的),所以并无任何“节省”,反而还颇有可能下降了应用程序的性能,由于额外的线程切换会形成性能损失。
  • DNS操做:BeginGetHostByName、BeginResolve。
  • Socket操做:BeginAccept、BeginConnect、BeginReceive等等。
  • WebRequest操做:BeginGetRequestStream、BeginGetResponse。
  • SqlCommand操做:BeginExecuteReader、BeginExecuteNonQuery等等。这多是开发一个Web应用时最经常使用的异步操做了。若是须要在执行数据库操做时获得IOCP支持,那么须要在链接字符串中标记Asynchronous Processing为true(默认为false),不然在调用BeginXXX操做时就会抛出异常。
  • WebServcie调用操做:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。

有一点我想再强调一下,那就是委托的BeginInvoke方法并不能得到IOCP支持,这会使用一个额外的线程来执行任务,这样不但没有节省,返而会下降性能。还有一点可能须要注意,IOCP的确能够不占用线程,可是一个真正的异步操做也不能毁在咱们的代码中。例如我曾经看到过以下的代码:

SqlCommand command;

IAsyncResult ar = command.BeginExecuteNonQuery();
int result = command.EndExecuteNonQuery(ar);

虽然在调用BeginExecuteNonQuery方法以后的确得到了IOCP的支持,可是以后调用的EndExecuteNonQuery却会阻塞当前线程直至数据库操做返回——异步操做不是这样用的。至于正确的作法,网络上已经有很多文章讲述了如何在ASP.NET中正确使用异步操做,你们能够搜索相应的资料来看,我也会在之后的文章中略有提到。

关于异步操做,此次就讲到这里吧。

相关文章
相关标签/搜索