在多線程的程序中,經(jīng)常會(huì)出現(xiàn)兩種情況:?
? ? ? 1. 應(yīng)用程序中線程把大部分的時(shí)間花費(fèi)在等待狀態(tài),等待某個(gè)事件發(fā)生,然后給予響應(yīng)拆宛。這一般使用ThreadPool(線程池)來解決嗓奢。
? ? ? 2. 線程平時(shí)都處于休眠狀態(tài)讼撒,只是周期性地被喚醒。這一般使用Timer(定時(shí)器)來解決股耽。
線程 Thread 有兩種:【前臺(tái)線程】和【后臺(tái)線程】根盒,我們可以通過線程屬性IsBackground?來指定線程的前后臺(tái)屬性(默認(rèn)是前臺(tái)線程);
兩者的主要區(qū)別是:進(jìn)程會(huì)等待所有的前臺(tái)線程完成后再結(jié)束工作物蝙,但是如果只剩下后臺(tái)線程炎滞,則會(huì)直接結(jié)束工作。
(一個(gè)重要注意事項(xiàng)是如果程序定義了一個(gè)不會(huì)完成的前臺(tái)線程诬乞,主程序并不會(huì)正常結(jié)束)
.Net環(huán)境使用 Thread 建立的線程默認(rèn)情況下為前臺(tái)線程册赛,即線程屬性 IsBackground = false;在進(jìn)程中震嫉,只要有一個(gè)前臺(tái)線程未退出森瘪,進(jìn)程就不會(huì)終止。主線程就是一個(gè)前臺(tái)線程票堵。后臺(tái)線程不管線程是否結(jié)束扼睬,只要所有前臺(tái)線程都退出(包括正常退出和異常退出)后,進(jìn)程就會(huì)自動(dòng)終止悴势。一般后臺(tái)線程用于處理時(shí)間較短的任務(wù)窗宇,如一個(gè)Web服務(wù)器中可以利用后臺(tái)線程來處理客戶端發(fā)過來的請(qǐng)求信息。而前臺(tái)線程用來處理需要長(zhǎng)時(shí)間等待的任務(wù)特纤,如在Web服務(wù)器中的監(jiān)聽客戶端請(qǐng)求的程序军俊,或是定時(shí)對(duì)某些系統(tǒng)資源進(jìn)行掃描的程序。
在什么情況下使用線程池捧存??
? ? ? 1.單個(gè)任務(wù)處理的時(shí)間比較短
? ? ? 2.將需處理的任務(wù)的數(shù)量大?
? ? ? 3.多線程的環(huán)境中粪躬,盡量采用線程池
線程池的大小
不管什么池官硝,總有尺寸,ThreadPool也不例外短蜕。ThreadPool提供了4個(gè)方法來調(diào)整線程池的大星饧堋:
SetMaxThreads
GetMaxThreads
SetMinThreads
GetMinThreads
SetMaxThreads指定線程池最多可以有多少個(gè)線程,而GetMaxThreads自然就是獲取這個(gè)值朋魔。SetMinThreads指定線程池中最少存活的線程的數(shù)量岖研,而GetMinThreads就是獲取這個(gè)值。
為何要設(shè)置一個(gè)最大數(shù)量和有一個(gè)最小數(shù)量呢警检?原來線程池的大小取決于若干因素孙援,如虛擬地址空間的大小等。比如你的計(jì)算機(jī)是4G內(nèi)存扇雕,而一個(gè)線程的初始堆棧大小為1M拓售,那么你最多能創(chuàng)建4G/1M的線程(忽略操作系統(tǒng)本身以及其他進(jìn)程內(nèi)存分配);正因?yàn)榫€程有內(nèi)存開銷镶奉,所以如果線程池的線程過多而又沒有被完全使用础淤,那么這就是對(duì)內(nèi)存的一種浪費(fèi),所以限制線程池的最大數(shù)是很make sense的哨苛。
那么最小數(shù)又是為啥鸽凶?線程池就是線程的對(duì)象池,對(duì)象池的最大的用處是重用對(duì)象建峭。為啥要重用線程玻侥,因?yàn)榫€程的創(chuàng)建與銷毀都要占用大量的CPU時(shí)間。所以在高并發(fā)狀態(tài)下亿蒸,線程池由于無需創(chuàng)建銷毀線程節(jié)約了大量時(shí)間凑兰,提高了系統(tǒng)的響應(yīng)能力和吞吐量。最小數(shù)可以讓你調(diào)整最小的存活線程數(shù)量來應(yīng)對(duì)不同的高并發(fā)場(chǎng)景边锁。
使用線程池的好處姑食?
在多線程編程時(shí),如果創(chuàng)建了過多的線程將會(huì)增加操作系統(tǒng)資源的占用砚蓬,并且還要處理資源要求和潛在的占用沖突矢门,并且使用了多線程之后將使代碼的執(zhí)行流程和資源競(jìng)爭(zhēng)情況變得復(fù)雜,稍不留心就會(huì)產(chǎn)生Bug灰蛙。在使用多線程編程時(shí)對(duì)需要同步的資源訪問尤其需要注意祟剔,如系統(tǒng)資源(系統(tǒng)端口等)、共享資源(文件摩梧、窗口句柄等)物延、屬于單個(gè)應(yīng)用程序的資源(如全局、靜態(tài)和實(shí)例字段或?qū)傩裕?/p>
針對(duì)上面的情況仅父,我們可以使用線程池來解決上面的大部分問題叛薯,跟使用單個(gè)線程相比浑吟,使用線程池有如下優(yōu)點(diǎn):
1、縮短應(yīng)用程序的響應(yīng)時(shí)間耗溜。因?yàn)樵诰€程池中有的線程處于等待分配任務(wù)狀態(tài)(只要沒有超過線程池的最大上限)组力,無需創(chuàng)建線程。
2抖拴、不必管理和維護(hù)生存周期短暫的線程燎字,不用在創(chuàng)建時(shí)為其分配資源,在其執(zhí)行完任務(wù)之后釋放資源阿宅。
3候衍、線程池會(huì)根據(jù)當(dāng)前系統(tǒng)特點(diǎn)對(duì)池內(nèi)的線程進(jìn)行優(yōu)化處理。
4洒放、減少在創(chuàng)建和銷毀線程上所花的時(shí)間以及系統(tǒng)資源的開銷蛉鹿,如不使用線程池,有可能造成系統(tǒng)創(chuàng)建大量線程而導(dǎo)致消耗完系統(tǒng)內(nèi)存以及”過度切換”往湿。
總之使用線程池的作用就是減少創(chuàng)建和銷毀線程的系統(tǒng)開銷妖异。在.NET中有一個(gè)線程的類ThreadPool,它提供了線程池的管理煌茴。
ThreadPool 類是一個(gè)靜態(tài)類随闺,你不能也不必要生成它的對(duì)象。而且一旦使用該方法在線程池中添加了一個(gè)項(xiàng)目蔓腐,那么該項(xiàng)目將是無法取消的。這里你無需自己建立線程龄句,只需把你要做的工作寫成函數(shù)回论,然后作為參數(shù)傳遞給ThreadPool.QueueUserWorkItem()方法就行了,傳遞的方法就是依靠 WaitCallback 代理對(duì)象分歇,而線程的建立傀蓉、管理、運(yùn)行等工作都是由系統(tǒng)自動(dòng)完成的职抡,你無須考慮那些復(fù)雜的細(xì)節(jié)問題葬燎。
ThreadPool是一個(gè)靜態(tài)類,它沒有構(gòu)造函數(shù)缚甩,對(duì)外提供的函數(shù)也全部是靜態(tài)的谱净。其中有一個(gè)QueueUserWorkItem方法,它有兩種重載形式擅威,如下:
public static bool QueueUserWorkItem(WaitCallback callBack):將方法排入隊(duì)列以便執(zhí)行壕探。此方法在有線程池線程變得可用時(shí)執(zhí)行。
public static bool QueueUserWorkItem(WaitCallback callBack,Object state):將方法排入隊(duì)列以便執(zhí)行郊丛,并指定包含該方法所用數(shù)據(jù)的對(duì)象李请。此方法在有線程池線程變得可用時(shí)執(zhí)行瞧筛。
QueueUserWorkItem方法中使用的的WaitCallback參數(shù)表示一個(gè)delegate,它的聲明如下:
public delegate void WaitCallback(Object state)
如果需要傳遞任務(wù)信息可以利用WaitCallback中的state參數(shù)导盅,類似于ParameterizedThreadStart委托较幌。
ThreadPool 的用法:?
首先程序創(chuàng)建了一個(gè) ManualResetEvent 對(duì)象,該對(duì)象就像一個(gè)信號(hào)燈白翻,可以利用它的信號(hào)來通知其它線程绅络。
ManualResetEvent 對(duì)象有幾個(gè)重要的方法:
初始化該對(duì)象時(shí),用戶可以指定其默認(rèn)的狀態(tài)(有信號(hào)/無信號(hào))嘁字;
在初始化以后恩急,該對(duì)象將保持原來的狀態(tài)不變,直到它的 Reset() 或者 Set() 方法被調(diào)用:?
? Reset():?
? ? ? ? ? ? ?將其設(shè)置為無信號(hào)狀態(tài)纪蜒;
? Set():?
? ? ? ? ? ? ?將其設(shè)置為有信號(hào)狀態(tài)衷恭。?
? WaitOne():?
?使當(dāng)前線程掛起,直到 ManualResetEvent 對(duì)象處于有信號(hào)狀態(tài)纯续,此時(shí)該線程將被激活随珠。然后,程序?qū)⑾蚓€程池中添加工作項(xiàng)猬错,這些以函數(shù)形式提供的工作項(xiàng)被系統(tǒng)用來初始化自動(dòng)建立的線程窗看。當(dāng)所有的線程都運(yùn)行完了以后,ManualResetEvent.Set() 方法被調(diào)用倦炒,因?yàn)檎{(diào)用了 ManualResetEvent.WaitOne() 方法而處在等待狀態(tài)的主線程將接收到這個(gè)信號(hào)显沈,于是它接著往下執(zhí)行,完成后邊的工作逢唤。
我們?cè)谝粋€(gè)程序中創(chuàng)建一個(gè)線程拉讯,安排給它一個(gè)任務(wù),便交由操作系統(tǒng)來調(diào)度執(zhí)行鳖藕。操作系統(tǒng)會(huì)管理系統(tǒng)中所有的線程魔慷,并且使用一定的方式進(jìn)行調(diào)度。什么是“調(diào)度”著恩?調(diào)度便是控制線程的狀態(tài):執(zhí)行院尔,等待等等。我們都知道喉誊,從理論上來說有多少個(gè)處理單元(如2 * 2 CPU的機(jī)器便有4個(gè)處理單元)邀摆,就表示操作系統(tǒng)可以同時(shí)做幾件事情。但是線程的數(shù)量會(huì)遠(yuǎn)遠(yuǎn)超過處理單元的數(shù)量裹驰,因此操作系統(tǒng)為了保證每個(gè)線程都被執(zhí)行隧熙,就必須等一個(gè)線程在某個(gè)處理器上執(zhí)行到某個(gè)情況的時(shí)候,“換”一個(gè)新的線程來執(zhí)行幻林,這便是所謂的“上下文切換(Context Switch)”贞盯。至于造成上下文切換的原因也有多種音念,可能是某個(gè)線程的邏輯決定的,如遇上鎖躏敢,或主動(dòng)進(jìn)入休眠狀態(tài)(調(diào)用Thread.Sleep方法)闷愤,但更有可能是操作系統(tǒng)發(fā)現(xiàn)這個(gè)線程“超時(shí)”了。在操作系統(tǒng)中會(huì)定義一個(gè)“時(shí)間片(timeslice)“件余,當(dāng)發(fā)現(xiàn)一個(gè)線程執(zhí)行時(shí)間超過這個(gè)時(shí)間讥脐,便會(huì)把它撤下,換上另外一個(gè)啼器。這樣看起來旬渠,多個(gè)線程——也就是多個(gè)任務(wù)在同時(shí)運(yùn)行了。值得一提的是端壳,對(duì)于Windows操作系統(tǒng)來說告丢,它的調(diào)度單元是線程,這和線程究竟屬于哪個(gè)進(jìn)程并沒有關(guān)系损谦。
QueueUserWorkItem這個(gè)技術(shù)存在許多限制岖免。其中最大的問題是沒有一個(gè)內(nèi)建的機(jī)制讓你知道操作在什么時(shí)候完成,也沒有一個(gè)機(jī)制在操作完成時(shí)獲得一個(gè)返回值照捡,這些問題使得我們都不敢啟用這個(gè)技術(shù)颅湘。