深度探索 Microsoft .NET提供的线程池, 揭示什么情况下你需要用线程池以及 .NET框架下的线程池是如何实现的,并告诉你如何去使用线程池。
如果你有在任何编程语言下的多线程编程经验的话,你肯定已经非常熟悉一些典型的范例。通常,多线程编程与基于用户界面的应用联系在一起,它们需要在不影响终端用户的情况下,执行一些耗时的操作。取出任何一本参考书,打开有关线程这一章:你能找到一个能在你的用户界面中并行执行数学运算的多线程示例吗?
我的目的不是让你扔掉你的书,不要这样做!多线程编程技术使基于用户界面的应用更完美。实际上, Microsoft .NET框架支持在任何语言编写的窗口下应用多线程编程技术,允许开发人员设计非常丰富的界面,提供给终端用户一个更好的体验。但是,多线程编程技术不仅仅是为了用户界面的应用,在没有任何用户界面的应用中,一样会出现多个执行流的情况。
我们用一个“硬件商店”的客户/服务器应用系统作为例子。客户端是收银机,服务端是运行在仓库里一台独立的机器上的应用系统。你可以想象一下,服务器没有任何的用户界面,如果不用多线程技术你将如何去实现?
服务端通过通道(http, sockets, files 等等)接收来自客户端的请求并处理它们,然后发送一个应答到客户端。图1显示了它是如何运作的。
图1: 单线程的服务端应用系统
为了让客户端的请求不会遗漏,服务端应用系统实现了某种队列来存放这些请求。图1显示了三个请求同时到达,但只有其中的一个被服务端处理。当服务端开始执行 "Decrease stock of monkey wrench," 这个请求时,其它两个必须在队列中等待。当第一个执行完成后,接着是第二个,以此类推。这种方法普遍用于许多现有的系统,但是这样做系统的资源利用率很低。假设 “decreasing the stock”请求修改磁盘上的一个文件,而这个文件正在被修改中,CPU将不会被使用,即使这个请求正处在待处理阶段。这类系统的一个普遍特征就是低CPU利用时间导致出现很长的响应时间,甚至是在访问压力很大的环境里也这样。
另外一个策略就是在当前的系统中为每一个请求创建不同的线程。当一个新的请求到达之后,服务端为进入的请求创建一个新线程,执行结束时,再销毁它。下图说明了这个过程:
图2:多线程服务端应用系统
就像如图2所示的那样。我们有了较高的CPU利用率。即使它已经不再像原来的那样慢了,但创建线和销毁程也不是最恰当的方法。假设线程的执行操作不复杂,由于需要花额外的时间去创建和销毁线程,所以最终会严重影响系统的响应时间。另外一点就是在压力很大的环境下,这三个线程会给系统带来很多的冲击。多个线程同时执行请求处理将导致CPU的利用率达到100%,而且大多数时间会浪费在上下文切换过程中,甚至会超过处理请求的本身。这类系统的典型特征是大量的访问会导致响应时间呈指数级增长和很高的CUP使用时间。
一个最优的实现是综合前面两种方案而提出的观点----线程池(Thread Pool),当一个请求达到时,应用系统把置入接收队列,一组的线程从队列提取请求并处理之。这个方案如下图所示:
图3:启用线程池的服务端应用系统
在这个例子中,我们用了一个含有两个线程的线程池。当三个请求到达时,它们立刻安排到队列等待被处理,因为两个线程都是空闲的,所以头两个请求开始执行。当其中任何一个请求处理结束后,空闲的线程就会去提取第三个请求并处理之。在这种场景中,系统不需要为每个请求创建和销毁线程。线程之间能互相利用。而且如果线程池的执行高效的话,它能增加或删除线程以获得最优的性能。例如当线程池在执行两个请求时,而CPU的利用率才达到50%,这表明执行请求正等待某个事件或者正在做某种I/O操作。线程池可以发现这种情况,并增加线程的数量以使系统能在同一时间处理更多的请求。相反的,如果CPU利用率达到100%,线程池可以减少线程的数量以获得更多的CPU时间,而不要浪费在上下文切换上面。
.NET中的线程池
基于上面的例子,在企业级应用系统中有一个高效执行的线程池是至关重要的。Microsoft在.NET框架的开发环境中已经实现了这个,该系统的核心提供了一个现成可用的最优线程池。
这个线程池不仅对应用程序可用,而且还融合到框架中的多数类中。.NET 建立在同一个池上是一个很重要的功能特性。比如 .NET Remoting 用它来处理来自远程对象的请求。
当一个托管应用程序开始执行时,运行时环境(runtime)提供一个线程池,它将在代码第一次访问时被创建。这个池与应用程序所在运行的物理进程关联在一起,当你用.NET框架下的同一进程中运行多个应用程序的功能特性时(称之为应用程序域),这将是一个很重要的细节。在这种情况下,由于它们都使用同样的线程池,一个坏的应用程序会影响进程中的其它应用程序。
你可以通过System.Threading 名称空间的Thread Pool 类来使用线程池,如果你查看一下这个类,就会发现所有的成员都是静态的,而且没有公开的构造函数。这是有理由这样做的,因为每个进程只有一个线程池,并且我们不能创建新的。这个限制的目的是为了把所有的异步编程技术都集中到同一个池中。所以我们不能拥有一个通过第三方组建创建的无法管理的线程池。