CLR线程池的做用与原理浅析

 线程池是一个重要的概念。不过我发现,关于这个话题的讨论彷佛还缺乏了点什么。做为资料的补充,以及从此文章所须要的引用,我在这里再完整而又简单地谈一下有关线程池,还有.NET中各类线程池的基础。更详细的内容就很少做展开了,有机会咱们再详细讨论这方面的细节。此次,仍是一个“概述”性质的,但愿能够说明白这方面问题的一些概念。程序员

线程池的做用web

其实“线程池”就是用来存放“线程”的对象池。数据库

在程序中,若是某个建立某种对象所须要的代价过高,同时这个对象又能够反复使用,那么咱们每每就会准备一个容器,用来保存一批这样的对象。因而乎,咱们想要用这种对象时,就不须要每次去建立一个,而直接从容器中取出一个现成的对象就能够了。因为节省了建立对象的开销,程序性能天然就上升了。这个容器就是“池”。很容易理解的是,由于有了对象池,所以在用完对象以后必须有一个“归还”的动做,这样即可以把对象放回池中,下次须要的时候就能够再次拿出来使用了。多线程

例如,咱们在使用ADO.NET链接SQL Server时,.NET框架就会自动帮咱们维护一个链接池,这就是由于从新建立一个链接的代价相对比较高昂,“复用”就显得比较划算了。不过有些朋友可能会说,咱们明明是每次都建立一个SqlConnection对象,哪里有“复用”啊?这是由于.NET框架中把“链接池”作透明了,对于程序员彻底隐藏了这个概念。每次咱们虽然建立的是新的SqlConnection对象,可是这个对象内部占用的“数据库链接”仍是会复用的。为何老是强调用完SqlConnection对象后要及时“关闭”(Dispose或Close)呢?其实这里并无断开数据库链接,只是把这个链接放回了链接池。等到下次建立新的SqlConnection对象时,这个链接又能够拿出来用了。框架

既然咱们每次都是从池中获取对象,那么这些对象是由谁来建立,又是何时建立的呢?这个就要根据不一样状况由各对象池来自行实现了。例如,能够在建立对象池的时候指定池内对象数量,而且一会儿所有建立好,固然您也能够在获得请求时,若是发现池中已经没有剩余对象时建立。您也能够“事前”先准备一部分,“事中”根据须要再继续补充。还能够作得“智能”一些,例如,根据实际状况添加或删除一些对象,甚至对需求“走势”进行“预测”,在空闲时便建立更多的对象以备“不时之需”。各中变化难以言尽。异步

固然,它们的原理和目的是相似的。相信上面这段文字也已经讲清了“线程池”的做用:由于建立一个线程的代价较高,所以咱们使用线程池设法复用线程。就是这么简单。ide

CLR线程池的做用性能

在.NET中,CLR线程和操做系统线程对应,您能够简单地认为.NET中的Thread对象便封装了一个操做系统线程,并附带一些托管环境下所须要的数据(如GC Handle)1。而CLR线程池即是存放这些CLR线程的对象池。spa

咱们在编写程序的时候,可使用ThreadPool类的两个静态方法:QueueUserWorkItem和UnsafeUserQueueWorkItem向CLR线程池中添加任务(一个WorkCallback委托对象),这两个方法的区别,在于前者会收集调用方的ExecutionContext,也就是保留了的当前线程的执行信息(如认证或语言文化等),使任务最终会在“建立”时刻的环境中执行2——后者就不会。所以,若是比较两个方法的绝对性能,Unsafe方法会略胜一筹。可是平时仍是建议使用QueueUserWorkItem方法,由于保留执行上下文会避免不少麻烦事情,且这点性能损耗其实算不上什么。操作系统

CLR线程池在.NET框架中的做用很大,除了让程序员使用以外,其余一些功能也会依赖CLR线程池。如ThreadPool.RegisterWaitForSingleObject方法,或是System.Threading.Timer组件——还有更重要可能也是更隐藏的:ASP.NET在获得一个请求后,也会将这个请求处理的任务交由CLR线程池去执行——请注意,它们最多只是添加任务而已,并不表示任务会当即执行。全部添加到CLR线程池的任务都会在合适的时候得以执行——可能立刻,也可能要稍等片刻,甚至更久。

向CLR线程池添加任务时,任务会被临时放到一个队列中,并在合适的时候执行。那么怎么样才算是“合适的时候”?简单的归纳说来,即是线程池内有空闲的线程,或线程池所管理的线程数量尚未达到上限的时候。若是有空闲的线程,线程池就会当即让它领取一个任务执行。若是是第二种状况,线程池便会建立新的Thread对象。因为让操做系统管理太多线程反而会形成性能降低,所以CLR线程池会有一个上限。不一样的托管环境会设置不一样的上限。如在.NET 2.0 SP1以后,普通的Windows应用程序(如控制台或WinForm/WPF),会将其设置为“处理器数 * 250”。也就是说,若是您的机器为2个2核CPU,那么CLR线程池的容量默认上限即是1000,也就是说,它最多能够管理1000个线程同时运行——不少状况下这已是一个很可怕的数字了,若是您以为这还不够,那么就应该考虑一下您的实现方式是否能够改进了。

对于ASP.NET应用程序来讲,CLR线程池容量表明了应用程序最多能够同时执行的请求数量。对于托管在IIS上的ASP.NET执行环境来讲,这个值由全局配置决定。这个配置在machine.config文件中system.web/processModel节点中,为maxWorkerThreads属性,它决定了为单个处理器分配的线程数。若是这个值为40,且机器上拥有4个处理器(2 * 2CPU),那么这台机器目前的配置表示在同一时刻,ASP.NET能够同时处理160个请求。某些参考资料建议您将其修改成每处理器80-100个线程,这时您只要修改相应的属性值就能够了。

既然有最大值,也就相应有了最小值,它表明了CLR线程池“老是会保留”的最少线程数量。因为线程会占用资源,如在默认状况下,每一个线程将得到1MB大小的栈空间3。因此若是在系统中保留太多空闲线程对资源也是一种浪费。所以,CLR线程池在使用大量线程处理完大量任务以后,也会逐步地释放线程,直至到达最小值。CLR线程池的最小线程数量确保了在任务数量较少的状况下,新来的任务能够当即执行,从而省去了建立新线程的时间。在普通应用程序中这个值为“处理器数 * 1”,而在ASP.NET应用程序中这个值配置在machine.config文件中system.web/processModel节点的minWorkerThreads属性中4。

在某些时候可能会遇到这样的状况:在一个瞬间突然来大量任务,每一个任务的执行时间说长不长说短不短,不过足以致使线程池快速分配数百个线程。若是这个峰值以后就一片平静,那么势必形成大量空闲的线程,这种开销对性能的损耗也很是明显。所以,CLR线程池限制了线程的建立速度不超过每秒2个。这样,即便在某个瞬时得到了大量的任务,CLR线程池也可使用相对较少的线程来完成全部工做5。

可是,还有一种状况也值得考虑。例如,对于一个比较繁忙的Web应用程序来讲,一打开便会涌入大量的链接。因为线程的建立速度有限,所以能够执行的请求数量也只能慢慢增长。对于这种您预料到会产生大量线程,并且忙碌情况会持续一段时间的状况,限制线程的建立速度反而会带来损伤效率。这时,您就能够手动设置CLR线程池的最小线程数量。若是此时CLR线程池中拥有的线程数量较少,那么系统就会当即建立必定数量的线程来达到这个最小值。设置和获取CLR线程池最小线程数量的接口为:

  
  
  
  
  1. public static class ThreadPool  
  2. {  
  3.     public static void GetMinThreads(out int workerThreads, out int completionPortThreads);  
  4.     public static bool SetMinThreads(int workerThreads, int completionPortThreads);  

这两个接口的做用和使用方式应该足够明显了(不理解的话能够查阅MSDN),其中workerThreads参数即是CLR线程池的最小线程数,而completionPortThreads涉及到咱们下次要讨论IO线程池,在此就很少做展开了。除了设置和读取CLR最小线程数的方法以外,ThreadPool还包含这些接口:

  
  
  
  
  1. public static class ThreadPool  
  2. {  
  3.     public static void GetMaxThreads(out int workerThreads, out int completionPortThreads);  
  4.     public static bool SetMaxThreads(int workerThreads, int completionPortThreads);  
  5.     public static void GetAvailableThreads(out int workerThreads, out int completionPortThreads);  

值得注意的是,不管是设置仍是获取到的这些数值,都与处理器数量没有任何关系了。也就是说,在一台2 * 2CPU的机器上运行一个普通的.NET应用程序时:

调用GetMaxThreads方法将得到1000,表示CLR线程池最大容量为1000(250 * 4),而不是250。

调用SetMinThreads并传入100,表示CLR线程池所拥有的最小线程数量为100,而不是400(100 * 4)。

对于CLR线程池的做用的简单描述就暂时先到这里了。若是您还有什么疑问请提出,我会加以补充。

注1:严格说来,Thread对象和系统线程对应关系还有些细节上的考虑。例如,Thread对象只有当真正Start了以后,CLR才会建立一个操做系统线程与它绑定。

注2:ExecutionContext是个很重要且颇有用的对象,例如,WinForms或WPF的异步任务中操做界面元素抛出异常该怎么办呢?

注3:使用Windows API或Thread类建立线程时能够指定它的栈空间大小,可是CLR线程池中的线程只能使用默认值——不过这个默认值也和托管环境有关,如普通应用程序默认为1MB,而ASP.NET为250KB,这意味着ASP.NET应用程序相对更容易产生Stack Overflow异常。

注4:惋惜的是,对于processModel节点的数据,ASP.NET只会读取machine.config中的全局配置信息,这意味着咱们不能使用web.config为不一样应用程序配置不一样的参数。若是咱们要实现应用程序级别的配置,那么必须使用ThreadPool类中提供的API进行设置,这点稍后便会提到。

注5:对于这点,您不妨来作一个算术题:线程池内一会儿涌入了500个任务,每一个任务阻塞或暂停5秒,每一个线程占用1MB内存,假设线程池目前为空,且有着足够的容量,此外线程建立速度也足够快,那么在限制及不限制线程建立速度的状况下,完成这些任务须要多少时间和内存空间?

相关文章
相关标签/搜索