Thread
目錄:
首先讓我們翻開書本來了解下線程的一些基礎知識:
1 線程有時被稱為輕量級進程,是程序執(zhí)行流的最小單元 2 線程時由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。 3 線程自身不能擁有系統(tǒng)資源,但是可以使用線程所屬進程所占有的系統(tǒng)資源 4 線程可以創(chuàng)建和撤銷另一個線程 5 線程可以擁有自身的狀態(tài),例如 運行狀態(tài),掛起狀態(tài),銷毀釋放狀態(tài)等等 6 線程具有優(yōu)先級,每個線程都分配了0-31 級別的其中一個優(yōu)先級,數(shù)字越大,優(yōu)先級越高,然而手動分配優(yōu)先級過于復雜, 所以微軟為我們的Thread類提供一個優(yōu)先級的枚舉,ThreadPriority枚舉便是優(yōu)先級枚舉,我們可以利用thread.Priority屬性來進行設置 7 線程開銷,這個是個復雜的話題,希望有機會的話能夠單獨寫一遍文章解釋下 |
那么多線程有什么實際好處呢?
首先讓我們了解下多線程的概念:一個程序或者進程中同時運行多個線程完成不同的工作
從概念中我們便可知道多線程的優(yōu)點了
1 能夠?qū)崿F(xiàn)并行操作,也就是說多個線程可以同時進行工作 2 利用多線程后許多復雜的業(yè)務或者是計算可以交給后臺線程去完成,從而提高整體程序的性能 3 類似于第一條利用多線程可以達到異步的作用(注意,實現(xiàn)異步的一種方式是多線程) |
當然多線程也有一定的問題需要注意,那就是線程同步問題,關于這個問題我會今后的文章中詳細說明
*1 線程同步
關于線程同步的概念最簡單的理解就是
同步方法調(diào)用在程序繼續(xù)執(zhí)行之前,需要等待同步方法執(zhí)行完畢返回結(jié)果
很有可能多個線程都會對一個資源進行訪問,從而導致資源被破壞,所以必須采用線程的同步機制,例如為共享資源加鎖 ,當其中一個線程占有了鎖之后,其余線程均不能使用共享資源,只有等其釋放鎖之后,接下來的其中一個線程會占有該 鎖,本系列會從Thread類開始講起,以后多章都會討論線程同步機制,例如鎖機制,臨界區(qū),互斥,信號量 同步事件等待句柄; 等等 |
*2 線程異步
線程異步指的是一個調(diào)用請求發(fā)送給被調(diào)用者,而調(diào)用者不用等待其結(jié)果的返回,一般異步執(zhí)行的任務都需要比較長的時間,
所以為了不影響主線程的工作,可以使用多線程或者新開辟一個線程來實現(xiàn)異步,同樣,異步和線程池也有著非常緊密的聯(lián)系,
這點我會在今后有關線程池的文章中詳細敘述,線程池和異步線程將在第二章中詳細闡述下
前臺線程:
諸如我們Console程序的主線程,wpf或者sliverlight的 界面線程等等,都屬于前臺線程,一旦前臺線程奔潰或者終止,相應的后臺
線程都會終止,本章中通過Thread類產(chǎn)生的線程默認都是前臺線程,當然我們可以設置Thread的屬性讓該對象成為后臺線程,必須
注意的是,一旦前臺線程全部運行完畢,應用程序的進程也會釋放,但是假設Console程序中main函數(shù)運行完畢,但是其中幾個前臺
線程還處在運行之中,那么這個Console程序的進程是不會釋放的,仍然處于運行之中,直到所有的前臺線程都釋放為止
后臺線程:
和前臺線程唯一的區(qū)別是,后臺線程更加默默無聞,甚至后臺線程因某種情況,釋放銷毀時不會影響到進程,也就是說后臺線程釋放時
不會導致進程的釋放
用一個例子再來說明下前后臺線程的區(qū)別: 有時我們打開outlook 后接受郵件時,程序會失去響應或被卡住,這時候我們?nèi)c擊outlook時系統(tǒng)會提示 outlook 失去響應,是否等待或者關閉, 當我們點擊關閉時,其實在程序中關于outlook的所有運行的前臺線程被終止,導致了outlook被關閉了,其進程也隨之釋放消失。但是,當我們在 outlook中點擊更新郵件時,后臺線程會去收取郵件的工作,我們可以在此期間關閉 outlook接受新郵件的后臺線程,而不會導致整個outlook的關閉 |
相信大家再看過前幾章對于線程的介紹后,對線程應該有一個溫故的感覺,那么讓我們開始對thread這個線程類進行深層次的研究下,
首先要啟動一個線程必須將該線程將要做的任務告訴該線程,否則,線程會不知道干什么事導致線程無意義的開啟,浪費系統(tǒng)資源,果然,
Thread類的構造函數(shù)提供了以下的版本
ThreadStart 和 ParameterThreadStart 參數(shù)都是委托,所以可以看出委托其實就是方法的抽象,前者用于不帶參數(shù)的并且無返回值的
方法的抽象,后者是帶object參數(shù)的方法的抽象,大家通過以下簡單的方法注意下線程如何調(diào)用帶參數(shù)的方法
public class ThreadStartTest { //無參數(shù)的構造函數(shù) Thread thread = new Thread(new ThreadStart(ThreadMethod)); //帶有object參數(shù)的構造函數(shù) Thread thread2 = new Thread(new ParameterizedThreadStart(ThreadMethodWithPara)); public ThreadStartTest() { //啟動線程1 thread.Start(); //啟動線程2 thread2.Start(new Parameter { paraName="Test" }); } static void ThreadMethod() { //.... } static void ThreadMethodWithPara(object o) { if (o is Parameter) { // (o as Parameter).paraName............. } } } public class Parameter { public string paraName { get; set; } }
不帶參數(shù)的方法似乎很簡單的能被調(diào)用,只要通過第一個構造函數(shù)便行,對于帶參數(shù)的方法,大家注意下參數(shù)是如何傳入線程所調(diào)用的方法,
當啟動線程時,參數(shù)通過thread.Start方法傳入,于是我們便成功啟動了thread線程,大伙可千萬不要小看基礎啊,往往在復雜的項目中很多
就是因為一些基礎導致,所以一定不要忽視它。。。
話說微軟對Thread.Sleep方法的解釋過于簡單,導致許多人會誤認為這個方法并不重要,其實這是錯誤的,其實線程是非常復雜的,
而且我們圍繞這個方法來溫故下windows系統(tǒng)對于CPU競爭的策略:
所謂搶占式操作系統(tǒng),就是說如果一個進程得到了 CPU 時間,除非它自己放棄使用 CPU ,否則將完全霸占 CPU 。因此可以看出,
在搶占式操作系統(tǒng)中,操作系統(tǒng)假設所有的進程都是“人品很好”的,會主動退出 CPU 。
發(fā)現(xiàn)寫到這里貌似真的已經(jīng)比較復雜了,由于本人對操作系統(tǒng)底層的知識比較匱乏,決定還是引用下別人的理解,順便自己也學習下
引用:
假設有源源不斷的蛋糕(源源不斷的時間),一副刀叉(一個CPU),10個等待吃蛋糕的人(10 個進程)。如果是 Unix 操作系統(tǒng)來負責分蛋糕, 那么他會這樣定規(guī)矩:每個人上來吃 1 分鐘,時間到了換下一個。最后一個人吃完了就再從頭開始。于是,不管這10個人是不是優(yōu)先級不同、饑餓 程度不同、飯量不同,每個人上來的時候都可以吃 1 分鐘。當然,如果有人本來不太餓,或者飯量小,吃了30秒鐘之后就吃飽了,那么他可以跟操 作系統(tǒng)說:我已經(jīng)吃飽了(掛起)。于是操作系統(tǒng)就會讓下一個人接 著來。如果是 Windows 操作系統(tǒng)來負責分蛋糕的,那么場面就很有意思了。 他會這樣定規(guī)矩:我會根據(jù)你們的優(yōu)先級、饑餓程度去給你們每個人計算一個優(yōu)先級。優(yōu)先級最高的那個人,可 以上來吃蛋糕——吃到你不想吃為止。 等這個人吃完了,我再重新根據(jù)優(yōu)先級、饑餓程度來計算每個人的優(yōu)先級,然后再分給優(yōu)先級最高的那個人。這樣看來,這個 場面就有意思了—— 可能有些人是PPMM,因此具有高優(yōu)先級,于是她就可以經(jīng)常來吃蛋糕。可能另外一個人的優(yōu)先級特別低,于是好半天了才輪到他一次(因為 隨著時間 的推移,他會越來越饑餓,因此算出來的總優(yōu)先級就會越來越高,因此總有一天會輪到他的)。而且,如果一不小心讓一個大胖子得到了刀叉,因為他 飯量 大,可能他會霸占著蛋糕連續(xù)吃很久很久,導致旁邊的人在那里咽口水。。。而且,還可能會有這種情況出現(xiàn):操作系統(tǒng)現(xiàn)在計算出來的結(jié)果,是 5號PPMM總優(yōu) 先級最高——高出別人一大截。因此就叫5號來吃蛋糕。5號吃了一小會兒,覺得沒那么餓了,于是說“我不吃了”(掛起)。因此操作系 統(tǒng)就會重新計算所有人的 優(yōu)先級。因為5號剛剛吃過,因此她的饑餓程度變小了,于是總優(yōu)先級變小了;而其他人因為多等了一會兒,饑餓程度都變大了, 所以總優(yōu)先級也變大了。不過這時 候仍然有可能5號的優(yōu)先級比別的都高,只不過現(xiàn)在只比其他的高一點點——但她仍然是總優(yōu)先級最高的啊。因此操作 系統(tǒng)就會說:5號mm上來吃蛋糕……(5號 mm心里郁悶,這不剛吃過嘛……人家要減肥……誰叫你長那么漂亮,獲得了那么高的優(yōu)先級)。那么, Thread.Sleep 函數(shù)是干嗎的呢?還用剛才的分蛋糕的場景來描述。上面的場景里面,5號MM在吃了一次蛋糕之后,覺得已經(jīng)有8分飽了,她覺得在未來 的半個小時之內(nèi)都不想再 來吃蛋糕了,那么她就會跟操作系統(tǒng)說:在未來的半個小時之內(nèi)不要再叫我上來吃蛋糕了。這樣,操作系統(tǒng)在隨后的半個小時 里面重新計算所有人總優(yōu)先級的時候, 就會忽略5號mm。Sleep函數(shù)就是干這事的,他告訴操作系統(tǒng)“在未來的多少毫秒內(nèi)我不參與CPU競爭”。 |
為什么我要把Thread.Join()方法單獨細說下,個人認為join方法非常重要,
在細說前我想再次強調(diào)下主線程和子線程的區(qū)別:
首先大家肯定知道在Console程序中,主線程自上而下著運行著main函數(shù),假如我們在main函數(shù)中新增一個線程thread對象的話,
也就是說,在主線程中再開啟一個子線程,同時子線程和主線程可以同時工作(前提是子線程使用Start方法),同理,假如我在這
個子線程中再開辟一個屬于這個子線程的子線程,同理這3個爺爺,父親,兒子線程也可以使用Start()方法一起工作,假如在主線
程中添加2個thread對象并開啟,那么這2 線程便屬于同一層次的線程(兄弟線程)(和優(yōu)先級無關,只同一位置層次上的兄弟),
有可能上述的讓你覺得郁悶或者難以理解?沒關系看簡單例子就能夠理解了
public static void ShowFatherAndSonThread(Thread grandFatherThread) { Console.WriteLine("爺爺主線程名:{0}", grandFatherThread.Name); Thread brotherThread = new Thread(new ThreadStart(() => { Console.WriteLine("兄弟線程名:{0}", Thread.CurrentThread.Name); })); Thread fatherThread = new Thread(new ThreadStart( () => { Console.WriteLine("父親線程名:{0}", Thread.CurrentThread.Name); Thread sonThread = new Thread(new ThreadStart(() => { Console.WriteLine("兒子線程名:{0}", Thread.CurrentThread.Name); })); sonThread.Name = "SonThread"; sonThread.Start(); } )); fatherThread.Name = "FatherThread"; brotherThread.Name="BrotherThread"; fatherThread.Start(); brotherThread.Start(); }
言歸正傳讓我們溫故下Jion方法,先看msdn中是怎么解釋的:
繼續(xù)執(zhí)行標準的 COM 和 SendMessage 消息泵處理期間,阻塞調(diào)用線程,直到某個線程終止為止。
大家把注意力移到后面紅色的部分,什么是“調(diào)用線程”呢?如果你理解上述線程關系的話,可能已經(jīng)理解了,主線程(爺爺輩)的調(diào)用了父親線程,
父親線程調(diào)用了兒子線程,假設現(xiàn)在我們有一個奇怪的需求,必須開啟爺爺輩和父親輩的線程但是,爺爺輩線程必須等待父親線程結(jié)束后再進行,
這該怎么辦? 這時候Join方法上場了,我們的目標是阻塞爺爺線程,那么后面的工作就明確了,讓父親線程(thread)對象去調(diào)用join方法就行
一下是個很簡單的例子,讓大家再深入理解下。
public static void ThreadJoin() { Console.WriteLine("我是爺爺輩線程,子線程馬上要來工作了我得準備下讓個位給他。"); Thread t1 = new Thread( new ThreadStart ( () => { for (int i = 0; i < 10; i++) { if (i == 0) Console.WriteLine("我是父親線層{0}, 完成計數(shù)任務后我會把工作權交換給主線程", Thread.CurrentThread.Name); else { Console.WriteLine("我是父親線層{0}, 計數(shù)值:{1}", Thread.CurrentThread.Name, i); } Thread.Sleep(1000); } } ) ); t1.Name = "線程1"; t1.Start(); //調(diào)用join后調(diào)用線程被阻塞
t1.Join(); Console.WriteLine("終于輪到爺爺輩主線程干活了"); }
代碼中當父親線程啟動后會立即進入Jion方法,這時候調(diào)用該線程爺爺輩線程被阻塞,直到父親線程中的方法執(zhí)行完畢為止,最后父親線程將控制
權再次還給爺爺輩線程,輸出最后的語句。聰明的你肯定會問:兄弟線程怎么保證先后順序呢?很明顯如果不使用join,一并開啟兄弟線程后結(jié)果
是隨機的不可預測的(暫時不考慮線程優(yōu)先級),但是我們不能在兄弟線程全都開啟后使用join,這樣阻塞了父親線程,而對兄弟線程是無效的,
其實我們可以變通一下,看以下一個很簡單的例子:
public static void ThreadJoin2() { IList<Thread> threads = new List<Thread>(); for (int i = 0; i < 3; i++) { Thread t = new Thread( new ThreadStart( () => { for (int j = 0; j < 10; j++) { if (j == 0) Console.WriteLine("我是線層{0}, 完成計數(shù)任務后我會把工作權交換給其他線程", Thread.CurrentThread.Name); else { Console.WriteLine("我是線層{0}, 計數(shù)值:{1}", Thread.CurrentThread.Name, j); } Thread.Sleep(1000); } })); t.Name = "線程" + i; //將線程加入集合 threads.Add(t); } foreach (var thread in threads) { thread.Start(); //每次按次序阻塞調(diào)用次方法的線程 thread.Join(); } }
輸出結(jié)果:
但是這樣我們即便能達到這種效果,也會發(fā)現(xiàn)其中存在著不少缺陷:
1:必須要指定順序
2:一旦一個運行了很久,后續(xù)的線程會一直等待很久
3: 很容易產(chǎn)生死鎖
從前面2個例子能夠看出 jion是利用阻塞調(diào)用線程的方式進行工作,我們可以根據(jù)需求的需要而靈活改變線程的運行順序,但是在復雜的項目或業(yè)務中
對于jion方法的調(diào)試和糾錯也是比較困難的。
7 細說下Thread 的 Abort和 Interrupt方法
Abort 方法:
其實 Abort 方法并沒有像字面上的那么簡單,釋放并終止調(diào)用線程,其實當一個線程調(diào)用 Abort方法時,會在調(diào)用此方法的線程上引發(fā)一個異常:
ThreadAbortException ,讓我們一步步深入下對這個方法的理解:
1 首先我們嘗試對主線程終止釋放
static void Main(string[] args) { try { Thread.CurrentThread.Abort(); } catch { //Thread.ResetAbort(); Console.WriteLine("主線程接受到被釋放銷毀的信號"); Console.WriteLine( "主線程的狀態(tài):{0}",Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("主線程最終被被釋放銷毀"); Console.WriteLine("主線程的狀態(tài):{0}", Thread.CurrentThread.ThreadState); Console.ReadKey(); }}
從運行結(jié)果上看很容易看出當主線程被終止時其實報出了一個ThreadAbortException, 從中我們可以進行捕獲,但是注意的是,主線程直到finally語
句塊執(zhí)行完畢之后才真正結(jié)束(可以仔細看下主線程的狀態(tài)一直處于AbortRequest),如果你在finally語句塊中執(zhí)行很復雜的邏輯或者計算的話,那
么只有等待直到運行完畢才真正銷毀主線程(也就是說主線程的狀態(tài)會變成Aborted,但是由于是主線程所以無法看出).
2 嘗試終止一個子線程
同樣先看下代碼:
static void TestAbort() { try { Thread.Sleep(10000); } catch { Console.WriteLine("線程{0}接受到被釋放銷毀的信號",Thread.CurrentThread.Name); Console.WriteLine("捕獲到異常時線程{0}主線程的狀態(tài):{1}", Thread.CurrentThread.Name,Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("進入finally語句塊后線程{0}主線程的狀態(tài):{1}", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } }Main:static void Main(string[] args) { Thread thread1 = new Thread(TestAbort); thread1.Name = "Thread1"; thread1.Start(); Thread.Sleep(1000); thread1.Abort(); thread1.Join(); Console.WriteLine("finally語句塊后,線程{0}主線程的狀態(tài):{1}", thread1.Name, thread1.ThreadState); Console.ReadKey(); }