浅谈线程池(上):线程池的作用及CLR线程池
線(xiàn)程池是一個(gè)重要的概念。不過(guò)我發(fā)現(xiàn),關(guān)于這個(gè)話(huà)題的討論似乎還缺少了點(diǎn)什么。作為資料的補(bǔ)充,以及今后文章所需要的引用,我在這里再完整而又簡(jiǎn)單地談一下有關(guān)線(xiàn)程池,還有.NET中各種線(xiàn)程池的基礎(chǔ)。更詳細(xì)的內(nèi)容就不多作展開(kāi)了,有機(jī)會(huì)我們?cè)僭敿?xì)討論這方面的細(xì)節(jié)。這次,還是一個(gè)“概述”性質(zhì)的,希望可以說(shuō)明白這方面問(wèn)題的一些概念。
線(xiàn)程池的作用
其實(shí)“線(xiàn)程池”就是用來(lái)存放“線(xiàn)程”的對(duì)象池。
在程序中,如果某個(gè)創(chuàng)建某種對(duì)象所需要的代價(jià)太高,同時(shí)這個(gè)對(duì)象又可以反復(fù)使用,那么我們往往就會(huì)準(zhǔn)備一個(gè)容器,用來(lái)保存一批這樣的對(duì)象。于是乎,我們想要用這種對(duì)象時(shí),就不需要每次去創(chuàng)建一個(gè),而直接從容器中取出一個(gè)現(xiàn)成的對(duì)象就可以了。由于節(jié)省了創(chuàng)建對(duì)象的開(kāi)銷(xiāo),程序性能自然就上升了。這個(gè)容器就是“池”。很容易理解的是,因?yàn)橛辛藢?duì)象池,因此在用完對(duì)象之后必須有一個(gè)“歸還”的動(dòng)作,這樣便可以把對(duì)象放回池中,下次需要的時(shí)候就可以再次拿出來(lái)使用了。
例如,我們?cè)谑褂肁DO.NET連接SQL Server時(shí),.NET框架就會(huì)自動(dòng)幫我們維護(hù)一個(gè)連接池,這就是因?yàn)橹匦聞?chuàng)建一個(gè)連接的代價(jià)相對(duì)比較高昂,“復(fù)用”就顯得比較劃算了。不過(guò)有些朋友可能會(huì)說(shuō),我們明明是每次都創(chuàng)建一個(gè)SqlConnection對(duì)象,哪里有“復(fù)用”啊?這是因?yàn)?NET框架中把“連接池”做透明了,對(duì)于程序員完全隱藏了這個(gè)概念。每次我們雖然創(chuàng)建的是新的SqlConnection對(duì)象,但是這個(gè)對(duì)象內(nèi)部占用的“數(shù)據(jù)庫(kù)連接”還是會(huì)復(fù)用的。為什么總是強(qiáng)調(diào)用完SqlConnection對(duì)象后要及時(shí)“關(guān)閉”(Dispose或Close)呢?其實(shí)這里并沒(méi)有斷開(kāi)數(shù)據(jù)庫(kù)連接,只是把這個(gè)連接放回了連接池。等到下次創(chuàng)建新的SqlConnection對(duì)象時(shí),這個(gè)連接又可以拿出來(lái)用了。
既然我們每次都是從池中獲取對(duì)象,那么這些對(duì)象是由誰(shuí)來(lái)創(chuàng)建,又是什么時(shí)候創(chuàng)建的呢?這個(gè)就要根據(jù)不同情況由各對(duì)象池來(lái)自行實(shí)現(xiàn)了。例如,可以在創(chuàng)建對(duì)象池的時(shí)候指定池內(nèi)對(duì)象數(shù)量,并且一下子全部創(chuàng)建好,當(dāng)然您也可以在得到請(qǐng)求時(shí),如果發(fā)現(xiàn)池中已經(jīng)沒(méi)有剩余對(duì)象時(shí)創(chuàng)建。您也可以“事前”先準(zhǔn)備一部分,“事中”根據(jù)需要再繼續(xù)補(bǔ)充。還可以做得“智能”一些,例如,根據(jù)實(shí)際情況添加或刪除一些對(duì)象,甚至對(duì)需求“走勢(shì)”進(jìn)行“預(yù)測(cè)”,在空閑時(shí)便創(chuàng)建更多的對(duì)象以備“不時(shí)之需”。各中變化難以言盡。
當(dāng)然,它們的原理和目的是類(lèi)似的。相信上面這段文字也已經(jīng)講清了“線(xiàn)程池”的作用:因?yàn)閯?chuàng)建一個(gè)線(xiàn)程的代價(jià)較高,因此我們使用線(xiàn)程池設(shè)法復(fù)用線(xiàn)程。就是這么簡(jiǎn)單。
CLR線(xiàn)程池
在.NET中,CLR線(xiàn)程和操作系統(tǒng)線(xiàn)程對(duì)應(yīng),您可以簡(jiǎn)單地認(rèn)為.NET中的Thread對(duì)象便封裝了一個(gè)操作系統(tǒng)線(xiàn)程,并附帶一些托管環(huán)境下所需要的數(shù)據(jù)(如GC Handle)1。而CLR線(xiàn)程池便是存放這些CLR線(xiàn)程的對(duì)象池。
我們?cè)诰帉?xiě)程序的時(shí)候,可以使用ThreadPool類(lèi)的兩個(gè)靜態(tài)方法:QueueUserWorkItem和UnsafeUserQueueWorkItem向CLR線(xiàn)程池中添加任務(wù)(一個(gè)WorkCallback委托對(duì)象),這兩個(gè)方法的區(qū)別,在于前者會(huì)收集調(diào)用方的ExecutionContext,也就是保留了的當(dāng)前線(xiàn)程的執(zhí)行信息(如認(rèn)證或語(yǔ)言文化等),使任務(wù)最終會(huì)在“創(chuàng)建”時(shí)刻的環(huán)境中執(zhí)行2——后者就不會(huì)。因此,如果比較兩個(gè)方法的絕對(duì)性能,Unsafe方法會(huì)略勝一籌。但是平時(shí)還是建議使用QueueUserWorkItem方法,因?yàn)楸A魣?zhí)行上下文會(huì)避免很多麻煩事情,且這點(diǎn)性能損耗其實(shí)算不上什么。
CLR線(xiàn)程池在.NET框架中的作用很大,除了讓程序員使用之外,其他一些功能也會(huì)依賴(lài)CLR線(xiàn)程池。如ThreadPool.RegisterWaitForSingleObject方法,或是System.Threading.Timer組件——還有更重要可能也是更隱藏的:ASP.NET在得到一個(gè)請(qǐng)求后,也會(huì)將這個(gè)請(qǐng)求處理的任務(wù)交由CLR線(xiàn)程池去執(zhí)行——請(qǐng)注意,它們最多只是添加任務(wù)而已,并不表示任務(wù)會(huì)立即執(zhí)行。所有添加到CLR線(xiàn)程池的任務(wù)都會(huì)在合適的時(shí)候得以執(zhí)行——可能馬上,也可能要稍等片刻,甚至更久。
向CLR線(xiàn)程池添加任務(wù)時(shí),任務(wù)會(huì)被臨時(shí)放到一個(gè)隊(duì)列中,并在合適的時(shí)候執(zhí)行。那么怎么樣才算是“合適的時(shí)候”?簡(jiǎn)單的概括說(shuō)來(lái),便是線(xiàn)程池內(nèi)有空閑的線(xiàn)程,或線(xiàn)程池所管理的線(xiàn)程數(shù)量還沒(méi)有達(dá)到上限的時(shí)候。如果有空閑的線(xiàn)程,線(xiàn)程池就會(huì)立即讓它領(lǐng)取一個(gè)任務(wù)執(zhí)行。如果是第二種情況,線(xiàn)程池便會(huì)創(chuàng)建新的Thread對(duì)象。由于讓操作系統(tǒng)管理太多線(xiàn)程反而會(huì)造成性能下降,因此CLR線(xiàn)程池會(huì)有一個(gè)上限。不同的托管環(huán)境會(huì)設(shè)置不同的上限。如在.NET 2.0 SP1之后,普通的Windows應(yīng)用程序(如控制臺(tái)或WinForm/WPF),會(huì)將其設(shè)置為“處理器數(shù) * 250”。也就是說(shuō),如果您的機(jī)器為2個(gè)2核CPU,那么CLR線(xiàn)程池的容量默認(rèn)上限便是1000,也就是說(shuō),它最多可以管理1000個(gè)線(xiàn)程同時(shí)運(yùn)行——很多情況下這已經(jīng)是一個(gè)很可怕的數(shù)字了,如果您覺(jué)得這還不夠,那么就應(yīng)該考慮一下您的實(shí)現(xiàn)方式是否可以改進(jìn)了。
對(duì)于ASP.NET應(yīng)用程序來(lái)說(shuō),CLR線(xiàn)程池容量代表了應(yīng)用程序最多可以同時(shí)執(zhí)行的請(qǐng)求數(shù)量。對(duì)于托管在IIS上的ASP.NET執(zhí)行環(huán)境來(lái)說(shuō),這個(gè)值由全局配置決定。這個(gè)配置在machine.config文件中system.web/processModel節(jié)點(diǎn)中,為maxWorkerThreads屬性,它決定了為單個(gè)處理器分配的線(xiàn)程數(shù)。如果這個(gè)值為40,且機(jī)器上擁有4個(gè)處理器(2 * 2CPU),那么這臺(tái)機(jī)器目前的配置表示在同一時(shí)刻,ASP.NET可以同時(shí)處理160個(gè)請(qǐng)求。某些參考資料建議您將其修改為每處理器80-100個(gè)線(xiàn)程,這時(shí)您只要修改相應(yīng)的屬性值就可以了。
既然有最大值,也就相應(yīng)有了最小值,它代表了CLR線(xiàn)程池“總是會(huì)保留”的最少線(xiàn)程數(shù)量。由于線(xiàn)程會(huì)占用資源,如在默認(rèn)情況下,每個(gè)線(xiàn)程將獲得1MB大小的棧空間3。所以如果在系統(tǒng)中保留太多空閑線(xiàn)程對(duì)資源也是一種浪費(fèi)。因此,CLR線(xiàn)程池在使用大量線(xiàn)程處理完大量任務(wù)之后,也會(huì)逐步地釋放線(xiàn)程,直至到達(dá)最小值。CLR線(xiàn)程池的最小線(xiàn)程數(shù)量確保了在任務(wù)數(shù)量較少的情況下,新來(lái)的任務(wù)可以立即執(zhí)行,從而省去了創(chuàng)建新線(xiàn)程的時(shí)間。在普通應(yīng)用程序中這個(gè)值為“處理器數(shù) * 1”,而在ASP.NET應(yīng)用程序中這個(gè)值配置在machine.config文件中system.web/processModel節(jié)點(diǎn)的minWorkerThreads屬性中4。
在某些時(shí)候可能會(huì)遇到這樣的情況:在一個(gè)瞬間忽然來(lái)大量任務(wù),每個(gè)任務(wù)的執(zhí)行時(shí)間說(shuō)長(zhǎng)不長(zhǎng)說(shuō)短不短,不過(guò)足以導(dǎo)致線(xiàn)程池快速分配數(shù)百個(gè)線(xiàn)程。如果這個(gè)峰值之后就一片平靜,那么勢(shì)必造成大量空閑的線(xiàn)程,這種開(kāi)銷(xiāo)對(duì)性能的損耗也非常明顯。因此,CLR線(xiàn)程池限制了線(xiàn)程的創(chuàng)建速度不超過(guò)每秒2個(gè)。這樣,即使在某個(gè)瞬時(shí)獲得了大量的任務(wù),CLR線(xiàn)程池也可以使用相對(duì)較少的線(xiàn)程來(lái)完成所有工作5。
但是,還有一種情況也值得考慮。例如,對(duì)于一個(gè)比較繁忙的Web應(yīng)用程序來(lái)說(shuō),一打開(kāi)便會(huì)涌入大量的連接。由于線(xiàn)程的創(chuàng)建速度有限,因此可以執(zhí)行的請(qǐng)求數(shù)量也只能慢慢增加。對(duì)于這種您預(yù)料到會(huì)產(chǎn)生大量線(xiàn)程,而且忙碌狀況會(huì)持續(xù)一段時(shí)間的情況,限制線(xiàn)程的創(chuàng)建速度反而會(huì)帶來(lái)?yè)p傷效率。這時(shí),您就可以手動(dòng)設(shè)置CLR線(xiàn)程池的最小線(xiàn)程數(shù)量。如果此時(shí)CLR線(xiàn)程池中擁有的線(xiàn)程數(shù)量較少,那么系統(tǒng)就會(huì)立即創(chuàng)建一定數(shù)量的線(xiàn)程來(lái)達(dá)到這個(gè)最小值。設(shè)置和獲取CLR線(xiàn)程池最小線(xiàn)程數(shù)量的接口為:
public static class ThreadPool {public static void GetMinThreads(out int workerThreads, out int completionPortThreads);public static bool SetMinThreads(int workerThreads, int completionPortThreads); }這兩個(gè)接口的作用和使用方式應(yīng)該足夠明顯了(不理解的話(huà)可以查閱MSDN),其中workerThreads參數(shù)便是CLR線(xiàn)程池的最小線(xiàn)程數(shù),而completionPortThreads涉及到我們下次要討論IO線(xiàn)程池,在此就不多作展開(kāi)了。除了設(shè)置和讀取CLR最小線(xiàn)程數(shù)的方法之外,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); }值得注意的是,無(wú)論是設(shè)置還是獲取到的這些數(shù)值,都與處理器數(shù)量沒(méi)有任何關(guān)系了。也就是說(shuō),在一臺(tái)2 * 2CPU的機(jī)器上運(yùn)行一個(gè)普通的.NET應(yīng)用程序時(shí):
- 調(diào)用GetMaxThreads方法將獲得1000,表示CLR線(xiàn)程池最大容量為1000(250 * 4),而不是250。
- 調(diào)用SetMinThreads并傳入100,表示CLR線(xiàn)程池所擁有的最小線(xiàn)程數(shù)量為100,而不是400(100 * 4)。
對(duì)于CLR線(xiàn)程池的簡(jiǎn)單描述就暫時(shí)先到這里了。如果您還有什么疑問(wèn)請(qǐng)?zhí)岢?#xff0c;我會(huì)加以補(bǔ)充。
相關(guān)文章
- 淺談線(xiàn)程池(上):線(xiàn)程池的作用及CLR線(xiàn)程池
- 淺談線(xiàn)程池(中):獨(dú)立線(xiàn)程池的作用及IO線(xiàn)程池
- 淺談線(xiàn)程池(下):相關(guān)試驗(yàn)及注意事項(xiàng)
?
注1:嚴(yán)格說(shuō)來(lái),Thread對(duì)象和系統(tǒng)線(xiàn)程對(duì)應(yīng)關(guān)系還有些細(xì)節(jié)上的考慮。例如,Thread對(duì)象只有當(dāng)真正Start了之后,CLR才會(huì)創(chuàng)建一個(gè)操作系統(tǒng)線(xiàn)程與它綁定。
注2:ExecutionContext是個(gè)很重要且很有用的對(duì)象,例如,WinForms或WPF的異步任務(wù)中操作界面元素拋出異常該怎么辦呢?
注3:使用Windows API或Thread類(lèi)創(chuàng)建線(xiàn)程時(shí)可以指定它的棧空間大小,但是CLR線(xiàn)程池中的線(xiàn)程只能使用默認(rèn)值——不過(guò)這個(gè)默認(rèn)值也和托管環(huán)境有關(guān),如普通應(yīng)用程序默認(rèn)為1MB,而ASP.NET為250KB,這意味著ASP.NET應(yīng)用程序相對(duì)更容易產(chǎn)生Stack Overflow異常。
注4:可惜的是,對(duì)于processModel節(jié)點(diǎn)的數(shù)據(jù),ASP.NET只會(huì)讀取machine.config中的全局配置信息,這意味著我們不能使用web.config為不同應(yīng)用程序配置不同的參數(shù)。如果我們要實(shí)現(xiàn)應(yīng)用程序級(jí)別的配置,那么必須使用ThreadPool類(lèi)中提供的API進(jìn)行設(shè)置,這點(diǎn)稍后便會(huì)提到。
注5:對(duì)于這點(diǎn),您不妨來(lái)做一個(gè)算術(shù)題:線(xiàn)程池內(nèi)一下子涌入了500個(gè)任務(wù),每個(gè)任務(wù)阻塞或暫停5秒,每個(gè)線(xiàn)程占用1MB內(nèi)存,假設(shè)線(xiàn)程池目前為空,且有著足夠的容量,此外線(xiàn)程創(chuàng)建速度也足夠快,那么在限制及不限制線(xiàn)程創(chuàng)建速度的情況下,完成這些任務(wù)需要多少時(shí)間和內(nèi)存空間?
from:?http://blog.zhaojie.me/2009/07/thread-pool-1-the-goal-and-the-clr-thread-pool.html
總結(jié)
以上是生活随笔為你收集整理的浅谈线程池(上):线程池的作用及CLR线程池的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 浅谈尾递归的优化方式
- 下一篇: 浅谈线程池(中):独立线程池的作用及IO