浅谈线程池(中):独立线程池的做用及IO线程池

原文地址:http://blog.zhaojie.me/2009/07/thread-pool-2-dedicate-pool-and-io-pool.htmlhtml

上一篇文章中,咱们简单讨论了线程池的做用,以及CLR线程池的一些特性。不过关于线程池的基本概念尚未结束,此次咱们再来补充一些必要的信息,有助于咱们在程序中选择合适的使用方式。编程

独立线程池

上次咱们讨论到,在一个.NET应用程序中会有一个CLR线程池,可使用ThreadPool类中的静态方法来使用这个线程池。咱们只要使用QueueUserWorkItem方法向线程池中添加任务,线程池就会负责在合适的时候执行它们。咱们还讨论了CLR线程池的一些高级特性,例如对线程的最大和最小数量做限制,对线程建立时间做限制以免突发的大量任务消耗太多资源等等。缓存

那么.NET提供的线程池又有什么缺点呢?有些朋友说,一个重要的缺点就是功能太简单,例如只有一个队列,无法作到对多个队列做轮询,没法取消任务,没法设定任务优先级,没法限制任务执行速度等等。不过其实这些简单的功能,倒均可以经过在CLR线程池上增长一层(或者说,经过封装CLR线程池)来实现。例如,您可让放入CLR线程池中的任务,在执行时从几个自定义任务队列中挑选一个运行,这样便达到了对多个队列做轮询的效果。所以,在我看来,CLR线程池的主要缺点并不在此。服务器

我认为,CLR线程池的主要问题在于“大一统”,也就是说,整个进程内部几乎全部的任务都会依赖这个线程池。如前篇文章所说的那样,如Timer和WaitForSingleObject,还有委托的异步调用,.NET框架中的许多功能都依赖这个线程池。这个作法是合适的,可是因为开发人员对于统一的线程池没法作到精确控制,所以在一些特别的须要就没法知足了。举个最多见例子:控制运算能力。什么是运算能力?那么仍是从线程讲起吧1网络

咱们在一个程序中建立一个线程,安排给它一个任务,便交由操做系统来调度执行。操做系统会管理系统中全部的线程,而且使用必定的方式进行调度。什么是“调度”?调度即是控制线程的状态:执行,等待等等。咱们都知道,从理论上来讲有多少个处理单元(如2 * 2 CPU的机器便有4个处理单元),就表示操做系统能够同时作几件事情。可是线程的数量会远远超过处理单元的数量,所以操做系统为了保证每一个线程都被执行,就必须等一个线程在某个处理器上执行到某个状况的时候,“换”一个新的线程来执行,这即是所谓的“上下文切换(context switch)”。至于形成上下文切换的缘由也有多种,多是某个线程的逻辑决定的,如赶上锁,或主动进入休眠状态(调用Thread.Sleep方法),但更有多是操做系统发现这个线程“超时”了。在操做系统中会定义一个“时间片(timeslice)”2,当发现一个线程执行时间超过这个时间,便会把它撤下,换上另一个。这样看起来,多个线程——也就是多个任务在同时运行了。架构

值得一提的是,对于Windows操做系统来讲,它的调度单元是线程,这和线程究竟属于哪一个进程并无关系。举个例子,若是系统中只有两个进程,进程A有5个线程,而进程B有10个线程。在排除其余因素的状况下,进程B占有运算单元的时间即是进程A的两倍。固然,实际状况天然不会那么简单。例如不一样进程会有不一样的优先级,线程相对于本身所属的进程还会有个优先级;若是一个线程在许久没有执行的时候,或者这个线程刚从“锁”的等待中恢复,操做系统还会对这个线程的优先级做临时的提高——这一切都是牵涉到程序的运行状态,性能等状况的因素,有机会咱们在作展开。并发

如今您意识到线程数量意味着什么了没?没错,就是咱们刚才提到的“运算能力”。不少时候咱们能够简单的认为,在一样的环境下,一个任务使用的线程数量越多,它所得到的运算能力就比另外一个线程数量较少的任务要来得多。运算能力天然就涉及到任务执行的快慢。您能够设想一下,有一个生产任务,和一个消费任务,它们使用一个队列作临时存储。在理想状况下,生产和消费的速度应该保持相同,这样能够带来最好的吞吐量。若是生产任务执行较快,则队列中便会产生堆积,反之消费任务就会不断等待,吞吐量也会降低。所以,在实现的时候,咱们每每会为生产任务和消费任务分别指派独立的线程池,而且经过增长或减小线程池内线程数量来条件运算能力,使生产和消费的步调达到平衡。app

使用独立的线程池来控制运算能力的作法很常见,一个典型的案例即是SEDA架构:整个架构由多个Stage链接而成,每一个Stage均由一个队列和一个独立的线程池组成,调节器会根据队列中任务的数量来调节线程池内的线程数量,最终使应用程序得到优异的并发能力。框架

在Windows操做系统中,Server 2003及以前版本的API也只提供了进程内部单一的线程池,不过在Vista及Server 2008的API中,除了改进线程池的性能以外,还提供了在同一进程内建立多个线程池的接口。很惋惜,.NET直到现在的4.0版本,依旧没有提供构建独立线程池的功能。构造一个优秀的线程池是一件至关困难的事情,幸运的是,若是咱们须要这方面的功能,能够借助著名的SmartThreadPool,通过那么多年的考验,相信它已经足够成熟了。若是须要,咱们还能够对它作必定修改——毕竟在不一样状况下,咱们对线程池的要求也不彻底相同。异步

IO线程池

IO线程池即是为异步IO服务的线程池。

访问IO最简单的方式(如读取一个文件)即是阻塞的,代码会等待IO操做成功(或失败)以后才继续执行下去,一切都是顺序的。可是,阻塞式IO有不少缺点,例如让UI中止响应,形成上下文切换,CPU中的缓存也可能被清除甚至内存被交换到磁盘中去,这些都是明显影响性能的作法。此外,每一个IO都占用一个线程,容易致使系统中线程数量不少,最终限制了应用程序的伸缩性。所以,咱们会使用“异步IO”这种作法。

在使用异步IO时,访问IO的线程不会被阻塞,逻辑将会继续下去。操做系统会负责把结果经过某种方法通知咱们,通常说来,这种方式是“回调函数”。异步IO在执行过程当中是不占用应用程序的线程的,所以咱们能够用少许的线程发起大量的IO,因此应用程序的响应能力也能够有所提升。此外,同时发起大量IO操做在某些时候会有额外的性能优点,例如磁盘和网络能够同时工做而不互相冲突,磁盘还能够根据磁头的位置来访问就近的数据,而不是根据请求的顺序进行数据读取,这样能够有效减小磁头的移动距离。

Windows操做系统中有多种异步IO方式,可是性能最高,伸缩性最好的方式莫过于传说中的“IO完成端口(I/O Completion Port,IOCP)”了,这也是.NET中封装的惟一异步IO方式。大约一年半前,老赵写过一篇文章《正确使用异步操做》,其中除了描述计算密集型和IO密集型操做的区别和效果以外,还简单地讲述了IOCP与CLR交互的方式,摘录以下:

当咱们但愿进行一个异步的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就会尽快分配一个可用的线程用于继续接下去的任务。

不过事实上,使用Windows API编写IOCP很是复杂。而在.NET中,因为须要迎合标准的APM(异步编程模型),在使用方便的同时也放弃必定的控制能力。所以,在一些真正须要高吞吐量的时候(如编写服务器),很多开发人员仍是会选择直接使用Native Code编写相关代码。不过在绝大部分的状况下,.NET中利用IOCP的异步IO操做已经足以得到很是优秀的性能了。使用APM方式在.NET中使用异步IO很是简单,以下:

static void Main(string[] args)
{
    WebRequest request = HttpWebRequest.Create("http://www.cnblogs.com");
    request.BeginGetResponse(HandleAsyncCallback, request);
}

static void HandleAsyncCallback(IAsyncResult ar)
{
    WebRequest request = (WebRequest)ar.AsyncState;
    WebResponse response = request.EndGetResponse(ar);
    // more operations...
}

BeginGetResponse将发起一个利用IOCP的异步IO操做,并在结束时调用HandleAsyncCallback回调函数。那么,这个回调函数是由哪里的线程执行的呢?没错,就是传说中“IO线程池”的线程。.NET在一个进程中准备了两个线程池,除了上篇文章中所提到的CLR线程池以外,它还为异步IO操做的回调准备了一个IO线程池。IO线程池的特性与CLR线程池相似,也会动态地建立和销毁线程,而且也拥有最大值和最小值(能够参考上一篇文章列举出的API)。

只惋惜,IO线程池也仅仅是那“一整个”线程池,CLR线程池的缺点IO线程池也包罗万象。例如,在使用异步IO方式读取了一段文本以后,下一步操做每每是对其进行分析,这就进入了计算密集型操做了。但对于计算密集型操做来讲,若是使用整个IO线程池来执行,咱们没法有效的控制某项任务的运算能力。所以在有些时候,咱们在回调函数内部会把计算任务再次交还给独立的线程池。这么作从理论上看会增大线程调度的开销,不过实际状况还得看具体的评测数据。若是它真的成为影响性能的关键因素之一,咱们就可能须要使用Native Code来调用IOCP相关API,将回调任务直接交给独立的线程池去执行了。

咱们也可使用代码来操做IO线程池,例以下面这个接口即是向IO线程池递交一个任务:

public static class ThreadPool
{
    public static bool UnsafeQueueNativeOverlapped(NativeOverlapped* overlapped);
}

NativeOverlapped包含了一个IOCompletionCallback回调函数及一个缓冲对象,能够经过Overlapped对象建立。Overlapped会包含一个被固定的空间,这里“固定”的含义表示不会由于GC而致使地址改变,甚至不会被置换到硬盘上的Swap空间去。这么作的目的是迎合IOCP的要求,可是很明显它也会下降程序性能。所以,咱们在实际编程中几乎不会使用这个方法3

相关文章

 

注1:若是没有加以说明,咱们这里谈论的对象默认为XP及以上版本的Window操做系统。

注2:timeslice又被称为quantum,不一样操做系统中定义的这个值并不相同。在Windows客户端操做系统(XP,Vista)中时间片默认为2个clock interval,在服务器操做系统(2003,2008)中默认为12个clock interval(在主流系统上,1个clock interval大约10到15毫秒)。服务器操做系统使用较长的时间片,是由于通常服务器上运行的程序比客户端要少不少,且更注重性能和吞吐量,而客户端系统更注重响应能力——并且,若是您真须要的话,时间片的长度也是能够调整的。

注3:不过,若是程序中屡次复用单个NativeOverlapped对象的话,这个方法的性能会略微好于QueueUserWorkItem,听说WCF中便使用了这种方式——微软内部总有那么些技巧是咱们不知如何使用的,例如老赵记得以前查看ASP.NET AJAX源代码的时候,在MSDN中不当心发现一个接口描述大意是“预留方法,请不要在外部使用”。对此,咱们又能有什么办法呢?

相关文章
相关标签/搜索