淺談執行緒池(上):執行緒池的作用及CLR執行緒池

iDotNetSpace發表於2009-07-23

執行緒池是一個重要的概念。不過我發現,關於這個話題的討論似乎還缺少了點什麼。作為資料的補充,以及今後文章所需要的引用,我在這裡再完整而又簡單地談一下有關執行緒池,還有.NET中各種執行緒池的基礎。更詳細的內容就不多作展開了,有機會我們再詳細討論這方面的細節。這次,還是一個“概述”性質的,希望可以說明白這方面問題的一些概念。

執行緒池的作用

  其實“執行緒池”就是用來存放“執行緒”的物件池。

  在程式中,如果某個建立某種物件所需要的代價太高,同時這個物件又可以反覆使用,那麼我們往往就會準備一個容器,用來儲存一批這樣的物件。於是乎,我們想要用這種物件時,就不需要每次去建立一個,而直接從容器中取出一個現成的物件就可以了。由於節省了建立物件的開銷,程式效能自然就上升了。這個容器就是“池”。很容易理解的是,因為有了物件池,因此在用完物件之後必須有一個“歸還”的動作,這樣便可以把物件放回池中,下次需要的時候就可以再次拿出來使用了。

  例如,我們在使用ADO.NET連線SQL Server時,.NET框架就會自動幫我們維護一個連線池,這就是因為重新建立一個連線的代價相對比較高昂,“複用”就顯得比較划算了。不過有些朋友可能會說,我們明明是每次都建立一個SqlConnection物件,哪裡有“複用”啊?這是因為.NET框架中把“連線池”做透明瞭,對於程式設計師完全隱藏了這個概念。每次我們雖然建立的是新的SqlConnection物件,但是這個物件內部佔用的“資料庫連線”還是會複用的。為什麼總是強呼叫完SqlConnection物件後要及時“關閉”(Dispose或Close)呢?其實這裡並沒有斷開資料庫連線,只是把這個連線放回了連線池。等到下次建立新的SqlConnection物件時,這個連線又可以拿出來用了。

  既然我們每次都是從池中獲取物件,那麼這些物件是由誰來建立,又是什麼時候建立的呢?這個就要根據不同情況由各物件池來自行實現了。例如,可以在建立物件池的時候指定池內物件數量,並且一下子全部建立好,當然您也可以在得到請求時,如果發現池中已經沒有剩餘物件時建立。您也可以“事前”先準備一部分,“事中”根據需要再繼續補充。還可以做得“智慧”一些,例如,根據實際情況新增或刪除一些物件,甚至對需求“走勢”進行“預測”,在空閒時便建立更多的物件以備“不時之需”。各中變化難以言盡。

  當然,它們的原理和目的是類似的。相信上面這段文字也已經講清了“執行緒池”的作用:因為建立一個執行緒的代價較高,因此我們使用執行緒池設法複用執行緒。就是這麼簡單。

CLR執行緒池

  在.NET中,CLR執行緒和作業系統執行緒對應,您可以簡單地認為.NET中的Thread物件便封裝了一個作業系統執行緒,並附帶一些託管環境下所需要的資料(如GC Handle)1。而CLR執行緒池便是存放這些CLR執行緒的物件池。

  我們在編寫程式的時候,可以使用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執行緒池最小執行緒數量的介面為:

public static class ThreadPool
{
    public static void GetMinThreads(out int workerThreads, out int completionPortThreads);
    public static bool SetMinThreads(int workerThreads, int completionPortThreads);
}

  這兩個介面的作用和使用方式應該足夠明顯了(不理解的話可以查閱MSDN),其中workerThreads引數便是CLR執行緒池的最小執行緒數,而completionPortThreads涉及到我們下次要討論IO執行緒池,在此就不多作展開了。除了設定和讀取CLR最小執行緒數的方法之外,ThreadPool還包含這些介面:

public static class ThreadPool
{
    public static void GetMaxThreads(out int workerThreads, out int completionPortThreads);
    public static bool SetMaxThreads(int workerThreads, int completionPortThreads);
    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記憶體,假設執行緒池目前為空,且有著足夠的容量,此外執行緒建立速度也足夠快,那麼在限制及不限制執行緒建立速度的情況下,完成這些任務需要多少時間和記憶體空間?

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-609983/,如需轉載,請註明出處,否則將追究法律責任。

相關文章