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內存,假設線程池目前爲空,且有着足夠的容量,此外線程建立速度也足夠快,那麼在限制及不限制線程建立速度的狀況下,完成這些任務須要多少時間和內存空間?

相關文章
相關標籤/搜索