那么我们先来确认一个概念,那就是“线程”。请注意,如果没有特殊说明,本文中出现的“线程”所指的是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进行异步调用。
下面的例子则对应了另一种情况:
老赵的那个开餐馆的朋友在小赚一笔之后准备再开一家快餐店。快餐店和餐馆有个不同之处,那就是快餐店的食品生产了大都有机器完成。可惜在这种情况下那个朋友还是遇到了问题:机器数量绰绰有余,但是人手还是不够。原来现在的做法还是相当不科学:服务员知道客人需要的食品之后,就将原料塞入机器,并看着机器是如何将原料变为美味的。当机器的工作完成之后,服务员便将食品打包并送出,然后继续招待别的客人。老赵听后还是哭笑不得:为啥服务员不能在机器工作的时候就去招待别的客人呢?
与这个示例对应的可以是一个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中正确使用异步操作,大家可以搜索相应的资料来看,我也会在以后的文章中略有提到。
关于异步操作,这次就讲到这里吧。