C#.Net使用线程池(ThreadPool)与专用线程(Thread)
?
線程池(ThreadPool)使用起來很簡單,但它有一些限制:?
1. 線程池中所有線程都是后臺線程,如果進(jìn)程的所有前臺線程都結(jié)束了,所有的后臺線程就會停止。不能把入池的線程改為前臺線 程。?
2. 不能給入池的線程設(shè)置優(yōu)先級或名稱。?
3. 對于COM對象,入池的所有線程都是多線程單元(Multi-threaded apartment,MTA)線程。許多COM對象都需要單線程單元(Single -threaded apartment,STA)線程。?
4.入池的線程只能用于時(shí)間較短的任務(wù)。如果線程要一直運(yùn)行(如Word的拼寫檢查器線程),就應(yīng)使用Thread類創(chuàng)建一個(gè)線程。
?
高效線程使用圣典
?
?
嚴(yán)格來講,線程的系統(tǒng)開銷很大。系統(tǒng)必須為線程分配并初始化一個(gè)線程內(nèi)核對象,還必須為每個(gè)線程保留1mb的地址空間 (按需提交)用于線程的用戶模式堆棧,分配12kb左右的地址空間用于線程的內(nèi)核模式堆棧。然后,緊接著線程創(chuàng)建后,windows調(diào) 用進(jìn)程中每個(gè)dll都有的一個(gè)函數(shù)來通知進(jìn)程中所有的dll操作系統(tǒng)創(chuàng)建了一個(gè)新的線程。同樣,銷毀一個(gè)線程的開銷也不小:進(jìn)程 中的每個(gè)dll都要接收一個(gè)關(guān)于線程即將“死亡”的通知,而且內(nèi)核對象及堆棧還需釋放。
如果一臺計(jì)算機(jī)中只有一個(gè)cpu,那么在某一時(shí)刻只有一個(gè)線程可以運(yùn)行。windows必須跟蹤記錄線程對象,而且是不停地跟 蹤記錄每個(gè)線程對象。windows不得不決定cpu下次調(diào)度哪個(gè)線程來執(zhí)行。這個(gè)額外的代碼不得不每隔20ms左右執(zhí)行一次。windows使 cpu停止執(zhí)行一個(gè)線程的代碼,而開始執(zhí)行另一個(gè)線程的代碼的現(xiàn)象,我們稱之為上下文切換(context switch)。上下文切換的開 銷相當(dāng)大,因?yàn)椴僮飨到y(tǒng)必須執(zhí)行以下步驟:
1. 進(jìn)入內(nèi)核模式。
2. 將cpu的寄存器保存到當(dāng)前正在執(zhí)行的線程的內(nèi)核對象中。x86架構(gòu)的機(jī)器上cpu寄存器占了大約700字節(jié)的空間;x64架構(gòu) 的機(jī)器上cpu寄存器占了大約1240字節(jié)的空間;而在ia64架構(gòu)的機(jī)器上cpu寄存器占了大約2500字節(jié)的空間。
3. 需要一個(gè)自旋鎖(spin lock),確定下一次調(diào)度哪個(gè)線程,然后再釋放該自旋鎖。如果下一次調(diào)度的線程屬于另一個(gè)進(jìn) 程,那么此處的開銷會更大,因?yàn)椴僮飨到y(tǒng)必切換到虛擬地址空間。
4. 將即將運(yùn)行的線程的內(nèi)核對象的值加載到cpu寄存器中。
5. 退出內(nèi)核模式。
所有上述內(nèi)容都是純粹的開銷,導(dǎo)致windows操作系統(tǒng)和應(yīng)用程序的執(zhí)行速度比在單線程系統(tǒng)上的執(zhí)行速度慢。
綜合上述所有結(jié)果可得出以下結(jié)論:應(yīng)盡可能地限制線程的使用。如果創(chuàng)建的線程越多,給操作系統(tǒng)帶來的開銷就越大,所 有的東西也就運(yùn)行得越慢。另外,每個(gè)線程都需要資源(內(nèi)核對象占用的內(nèi)存及兩個(gè)堆棧),所以每個(gè)線程都會消耗內(nèi)存。
線程還有另一個(gè)用途:可擴(kuò)展性。當(dāng)計(jì)算機(jī)有多個(gè)cpu時(shí),windows能同時(shí)調(diào)度多個(gè)線程:每個(gè)cpu運(yùn)行一個(gè)線程。
CLR線程池簡介
如前所述,創(chuàng)建并銷毀一個(gè)線程在時(shí)間上的開銷相當(dāng)大。另外,線程多還會浪費(fèi)內(nèi)存資源,而且由于操作系統(tǒng)不得不在可運(yùn) 行線程間進(jìn)行調(diào)度和上下文切換,從而影響操作系統(tǒng)和應(yīng)用程序的性能。為改進(jìn)這種現(xiàn)象,clr中包含管理clr線程池的代碼。我們 可以將線程池看作應(yīng)用程序自己使用的線程的集合。每個(gè)進(jìn)程都有一個(gè)線程池,這個(gè)線程池被該進(jìn)程中的所有應(yīng)用程序域共享。
當(dāng)clr初始化時(shí),線程池中還沒有任何線程。從內(nèi)部實(shí)現(xiàn)上講,線程池維護(hù)了一系列操作請求。應(yīng)用程序希望執(zhí)行一個(gè)異步 操作時(shí),可以調(diào)用一些方法在線程池的隊(duì)列中加入一個(gè)條目。線程池中的代碼將從這個(gè)隊(duì)列中提取出條目,并將該條目分派到線程 池中的線程。如果線程池中沒有任何線程,就創(chuàng)建一個(gè)新的線程。創(chuàng)建一個(gè)線程會有相關(guān)的性能損失。但是,當(dāng)線程池中的線程完 成任務(wù)時(shí),并不會被銷毀,而是返回到線程池中,在線程池中空閑,等待響應(yīng)另外的請求。因?yàn)榫€程不對它自身進(jìn)行銷毀,所以此 處不會帶來性能損失。
如果應(yīng)用程序?qū)€程池進(jìn)行了很多的請求,那么線程池將試圖只用一個(gè)線程來響應(yīng)所有的請求。但是,如果應(yīng)用程序排隊(duì)的 請求超出了線程池的處理能力,線程池中將創(chuàng)建另外的線程。最終,應(yīng)用程序排隊(duì)的請求與線程池中線程的處理能力達(dá)到一個(gè)平衡 點(diǎn),我們可以采用較小數(shù)量的線程來處理所有的請求,因此線程池中也就不再需要創(chuàng)建更多的線程。
如果應(yīng)用程序停止請求線程池,線程池中可能會有許多不做事情的線程。這種情況會浪費(fèi)內(nèi)存資源。因此,當(dāng)線程池中的線 程空閑超過大約2分鐘后,線程將喚醒自己,并終止自己,以釋放內(nèi)存資源。當(dāng)線程終止自己時(shí),也會存在一個(gè)性能損失。但是,該 性能損失不是很嚴(yán)重,因?yàn)榫€程在終止自己時(shí),線程已處于空閑狀態(tài),這意味著我們的應(yīng)用程序當(dāng)前沒有執(zhí)行太多的工作。
從內(nèi)部實(shí)現(xiàn)上講,線程池將線程池中的線程進(jìn)行分類,劃分為工作線程(worker thread)和i/o線程(i/o thread)。當(dāng)應(yīng) 用程序請求線程池執(zhí)行一個(gè)受計(jì)算限制的異步操作(包括初始化受i/o限制的異步操作)時(shí)使用工作線程,而i/o線程用于在受i/o限 制的異步操作完成時(shí)通知代碼。具體而言,這意味著我們需要使用異步編程模型來進(jìn)行i/o請求。
限制線程池中的線程數(shù)量
clr的線程池允許開發(fā)人員設(shè)置工作線程和i/o線程的最大數(shù)量。clr保證創(chuàng)建的線程數(shù)量不會超過這個(gè)設(shè)置值。但永遠(yuǎn)不要 對線程池中線程的數(shù)量設(shè)置一個(gè)上限,因?yàn)轲囸I和死鎖現(xiàn)象可能會發(fā)生。在clr的2.0版默認(rèn)中,工作線程的默認(rèn)最大數(shù)量為機(jī)器中 每個(gè)cpu25個(gè),i/o線程最大數(shù)量設(shè)為1000個(gè)。
system.threading.threadpool類提供了幾個(gè)操作線程池中線程數(shù)量的靜態(tài)方法:getmaxthreads(查詢線程池對線程數(shù)量的 最大限制)、setmax-threads(設(shè)置線程數(shù)量最大限制)、getminthreads(查詢線程池對線程數(shù)量的最小限制)、setminthreads (設(shè)置線程數(shù)量最小限制)、getavailable-threads。
強(qiáng)烈建議不要調(diào)用setmaxthreads方法修改線程池中線程數(shù)量的限制,因?yàn)檫@會導(dǎo)致?lián)p害應(yīng)用程序的執(zhí)行性能。
clr的線程池試圖避免過快地創(chuàng)建額外的線程。具體而言,線程池試圖避免每隔500ms就創(chuàng)建一個(gè)新的線程。這對某些開發(fā)人 員而言,引發(fā)了一個(gè)問題,因?yàn)殛?duì)列中的任務(wù)無法得到及時(shí)地處理。要處理此問題,可以調(diào)用setminthreads方法設(shè)置線程池中擁有 線程的最低數(shù)量。調(diào)用該方法后,線程池將很快地創(chuàng)建這么多的線程,并且當(dāng)隊(duì)列的任務(wù)繼續(xù)增加,所創(chuàng)建的所有線程都被使用后 ,線程池還會按照每隔500ms的時(shí)間繼續(xù)創(chuàng)建額外的線程。默認(rèn)情況下,線程池中工作線程和i/o線程的最小數(shù)量被設(shè)為2,這個(gè)值可 以通過調(diào)用getminthreads方法獲得。
最后,可以通過調(diào)用getavailablethreads方法來獲得線程池中可以增加的額外線程的數(shù)量。該方法的返回值為線程池中可 以擁有的線程的最大數(shù)量減去線程池中當(dāng)前所擁有的線程數(shù)量。這個(gè)值僅在返回的那一刻有用,因?yàn)樵诜椒ǚ祷睾?#xff0c;線程池中可能 已經(jīng)增加了許多線程,或有些線程可能已被銷毀。
使用線程池執(zhí)行受計(jì)算限制的異步操作
受計(jì)算限制的操作是需要進(jìn)行計(jì)算的操作。如,電子表格應(yīng)用程序中可計(jì)算的單元。理想情況下,受計(jì)算限制的操作不會執(zhí) 行任何異步i/o操作,因?yàn)樗械漠惒絠/o操作在底層硬件執(zhí)行工作時(shí)都將掛起調(diào)用線程。應(yīng)該盡量使線程運(yùn)行,因?yàn)閽炱鸬木€程不 再繼續(xù)運(yùn)行但仍然使用系統(tǒng)的資源。
為了將一個(gè)受計(jì)算限制的異步操作加入到線程池的隊(duì)列中,一般可以使用threadpool類中定義的下述方法:
?
static bool QueueUserWorkItem(WaitCallback callback);
static bool QueueUserWorkItem(WaitCallback callback, object state);
static bool UnsafeQueueUserWorkItem(WaitCallback callback, object state);
上述方法將一個(gè)“工作項(xiàng)”(及可選的狀態(tài)數(shù)據(jù))加入到線程池的隊(duì)列中,然后這些方法就會立即返回。工作項(xiàng)僅僅是一個(gè) 由callback參數(shù)標(biāo)識的方法,線程池中的線程將調(diào)用該方法。該方法可以只傳遞一個(gè)單獨(dú)的由state(狀態(tài)數(shù)據(jù))參數(shù)指定的參數(shù)。 沒有state參數(shù)的QueueUserWorkItem方法為回調(diào)函數(shù)傳遞null。最終,線程池中的一些線程將執(zhí)行工作項(xiàng),從而導(dǎo)致我們的方法被 調(diào)用。我們寫的回調(diào)方法必須匹配system.threading.WaitCallback委托類型,它的定義方式如下所示:
delegate void WaitCallback(object state);
下面的代碼演示了線程池中的線程如何異步調(diào)用一個(gè)方法:
?
using system;
using system.threading;
public static class program
{
public static void main()
{
console.writeline("main thread: queuing an asynchronous operation"); threadpool.QueueUserWorkItem(computeboundop, 5);
console.writeline("main thread: doing other work here ...");
thread.sleep(10000); //模擬其他工作10秒鐘
console.writeline("hit <enter> to end this program ...");
console.readline(); }
//該方法的簽名必須與WaitCallback委托類型匹配
private static void computeboundop(object state)
{
//該方法由線程池中的線程執(zhí)行
console.writeline("in computeboundop: state={0}", state);
thread.sleep(1000); //模擬其他工作1秒鐘
//在該方法返回后,線程就回到線程池中,然后等待執(zhí)行另一個(gè)任務(wù)
}
}
如果回調(diào)方法拋出的異常是未處理異常,那么clr將終止進(jìn)程。
threadpool類有一個(gè)UnsafeQueueUserWorkItem方法。該方法與平時(shí)調(diào)用的QueueUserWorkItem方法非常相似。下面先簡單介 紹一下這兩個(gè)方法的區(qū)別:試圖訪問一個(gè)受限資源(如打開一個(gè)文件)時(shí),clr將執(zhí)行一個(gè)代碼訪問安全(code access security, cas)檢查。也就是說,clr將檢查調(diào)用線程的調(diào)用堆棧中的所有程序集是否都有訪問資源的許可權(quán)限。如果有一些程序集沒有所需 的許可權(quán)限,clr將拋出一個(gè)securityexception異常。假設(shè)正在執(zhí)行代碼的線程所在的程序集沒有打開文件的許可權(quán)限,那么在線 程試圖打開文件時(shí),clr將拋出一個(gè)securityexception異常。
為讓線程繼續(xù)運(yùn)行,線程可以在線程池的隊(duì)列加入一個(gè)工作項(xiàng),讓線程池中的線程來執(zhí)行打開文件的代碼。當(dāng)然這必須在擁 有合適許可權(quán)限的程序集中進(jìn)行。這種“工作區(qū)”智取安全權(quán)限的現(xiàn)象可以允許懷惡意的代碼對受限資源進(jìn)行嚴(yán)重破壞。為阻止這 種獲得安全權(quán)限的方式,QueueUserWorkItem方法內(nèi)部遍歷調(diào)用線程的堆棧,并捕獲所有被授予的安全權(quán)限。然后,當(dāng)線程池中的線 程開始執(zhí)行時(shí),這些權(quán)限再與線程結(jié)合。因此,線程池中的線程以調(diào)用QueueUserWorkItem方法的線程相同的權(quán)限集來完成運(yùn)行。
遍歷線程的堆棧并捕獲所有的安全權(quán)限與性能緊密相關(guān)。如果希望改進(jìn)受計(jì)算限制的異步操作的排隊(duì)性能,可以調(diào)用 UnsafeQueueUserWorkItem方法。該方法只將工作項(xiàng)加入到線程池的隊(duì)列中,而不遍歷調(diào)用線程的堆棧。最后結(jié)果是這個(gè)方法比 QueueUserWorkItem方法執(zhí)行得快,但它在應(yīng)用程序中打開了一個(gè)潛在的安全漏洞。僅當(dāng)可以確認(rèn)線程池中的線程執(zhí)行的代碼不觸及 受限資源時(shí),或確信接觸這部分資源不會出現(xiàn)問題時(shí),我們才可以調(diào)用unsafequeueuserwork-item方法。同樣,還需注意調(diào)用該方 法需要使securitypermission的controlpolicy標(biāo)記和controlevidence標(biāo)記開啟,可阻止未信任的代碼偶然或故意提升它的許可權(quán) 限。
使用專用線程執(zhí)行受計(jì)算限制的異步操作
強(qiáng)烈建議大家盡量多用線程池來執(zhí)行受計(jì)算限制的異步操作。但在有些情況下,我們可能希望顯式創(chuàng)建一個(gè)線程,專門用于 執(zhí)行特定的受計(jì)算限制的異步操作。一般情況下,如果即將執(zhí)行的代碼需要線程處于一個(gè)特定的狀態(tài)(與線程池中線程的普通狀態(tài) 不同),那么就希望創(chuàng)建一個(gè)專用的線程。如:希望線程以一個(gè)特殊的優(yōu)先級運(yùn)行(所有線程池中的線程都以普通優(yōu)先級運(yùn)行,而 且我們不應(yīng)該修改線程池中線程的優(yōu)先級),就需要創(chuàng)建一個(gè)專用的線程。再如:希望讓一個(gè)線程成為前臺線程(所有線程中的線 程都是后臺線程),也可以考慮創(chuàng)建并使用自己的線程,從而阻止應(yīng)用程序的“死亡”,直到線程完成任務(wù)。如果受計(jì)算限制的任 務(wù)運(yùn)行的時(shí)間特別長,也應(yīng)該使用專用線程,這樣,我們就不必讓線程池的邏輯去費(fèi)力判斷是否還需創(chuàng)建額外的線程。最后,如果 我們希望啟動一個(gè)線程,然后通過調(diào)用thread的abort方法中斷該線程的話,應(yīng)該使用一個(gè)專用線程。
為創(chuàng)建一個(gè)專用線程,我們可構(gòu)建一個(gè)system.threading.thread類的實(shí)例(以方法的名稱作為構(gòu)造器的參數(shù))。下面是構(gòu) 造器的原型:
?
public sealed class thread : criticalfinalizerobject, ...
{
public thread(parameterizedthreadstart start);
}
參數(shù)start用來標(biāo)識專用線程的方法即將執(zhí)行,這個(gè)方法必須與委托parameterizedthreadstart的簽名相匹配:
?
delegate void parameterizedthreadstart(object obj);
可看出,parameterizedthreadstart委托的簽名與WaitCallback委托的簽名相同。這意味著使用一個(gè)線程池中的線程或使用 一個(gè)專用線程就可以調(diào)用相同的方法。
構(gòu)建一個(gè)thread對象并不創(chuàng)建一個(gè)操作系統(tǒng)線程。為實(shí)際創(chuàng)建一個(gè)操作系統(tǒng)線程,并讓它開始執(zhí)行回調(diào)方法,我們必須調(diào)用 thread的start方法。如下所示:
?
using system;
using system.threading;
public static class program
{
public static void main()
{
console.writeline("main thread: starting a dedicated thread " + " to do an asynchronous operation");
thread dedicatedthread = new thread(computeboundop);
dedicatedthread.start(5); console.writeline("main thread: doing other work here...");
thread.sleep(10000); //模擬其他工作10秒 dedicatedthread.join(); //等待線程終止 console.writeline("hit <enter> to end this program...");
console.readline();
}
//該方法的簽名必須與parameterizedthreadstart委托匹配
private static void computeboundop(object state)
{
//該方法由一個(gè)專用線程執(zhí)行
console.writeline("in computeboundop: state = {0}", state);
thread.sleep(1000); //模擬其他工作1秒
}
}
注意,main方法調(diào)用了join方法,而join方法導(dǎo)致調(diào)用線程停止執(zhí)行任何代碼,直到由dedicatedthread標(biāo)識的線程自己銷 毀自己或被終止。使用threadpool的QueueUserWorkItem方法將異步操作排隊(duì)時(shí),clr沒有提供內(nèi)置的方法來判斷操作是否完成。而 join方法卻在我們使用專用線程時(shí)為我們提供了這種能力。但是,如果需要知道操作是在什么時(shí)候完成的,就不應(yīng)該使用專用線程 來取代QueueUserWorkItem方法,而應(yīng)該使用apm。
轉(zhuǎn)載文章,C/S框架網(wǎng)責(zé)任編輯
?
總結(jié)
以上是生活随笔為你收集整理的C#.Net使用线程池(ThreadPool)与专用线程(Thread)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 殉职司机杨勇紧急制动画面曝光:5秒制动D
- 下一篇: 浦发信用卡万用金可以提前还款吗