C# 多线程控制 通讯 和切换
一.多線程的概念?
Windows是一個(gè)多任務(wù)的系統(tǒng),如果你使用的是windows 2000及其以上版本,你可以通過(guò)任務(wù)管理器查看當(dāng)前系統(tǒng)運(yùn)行的程序和進(jìn)程。什么是進(jìn)程呢?當(dāng)一個(gè)程序開(kāi)始運(yùn)行時(shí),它就是一個(gè)進(jìn)程,進(jìn)程所指包括運(yùn)行中的程序和程序所使用到的內(nèi)存和系統(tǒng)資源。而一個(gè)進(jìn)程又是由多個(gè)線程所組成的,線程是程序中的一個(gè)執(zhí)行流,每個(gè)線程都有自己的專有寄存器(棧指針、程序計(jì)數(shù)器等),但代碼區(qū)是共享的,即不同的線程可以執(zhí)行同樣的函數(shù)。多線程是指程序中包含多個(gè)執(zhí)行流,即在一個(gè)程序中可以同時(shí)運(yùn)行多個(gè)不同的線程來(lái)執(zhí)行不同的任務(wù),也就是說(shuō)允許單個(gè)程序創(chuàng)建多個(gè)并行執(zhí)行的線程來(lái)完成各自的任務(wù)。瀏覽器就是一個(gè)很好的多線程的例子,在瀏覽器中你可以在下載JAVA小應(yīng)用程序或圖象的同時(shí)滾動(dòng)頁(yè)面,在訪問(wèn)新頁(yè)面時(shí),播放動(dòng)畫(huà)和聲音,打印文件等。?
多線程的好處在于可以提高CPU的利用率——任何一個(gè)程序員都不希望自己的程序很多時(shí)候沒(méi)事可干,在多線程程序中,一個(gè)線程必須等待的時(shí)候,CPU可以運(yùn)行其它的線程而不是等待,這樣就大大提高了程序的效率。?
然而我們也必須認(rèn)識(shí)到線程本身可能影響系統(tǒng)性能的不利方面,以正確使用線程:
- 線程也是程序,所以線程需要占用內(nèi)存,線程越多占用內(nèi)存也越多
- 多線程需要協(xié)調(diào)和管理,所以需要CPU時(shí)間跟蹤線程
- 線程之間對(duì)共享資源的訪問(wèn)會(huì)相互影響,必須解決競(jìng)用共享資源的問(wèn)題
- 線程太多會(huì)導(dǎo)致控制太復(fù)雜,最終可能造成很多Bug
基于以上認(rèn)識(shí),我們可以一個(gè)比喻來(lái)加深理解。假設(shè)有一個(gè)公司,公司里有很多各司其職的職員,那么我們可以認(rèn)為這個(gè)正常運(yùn)作的公司就是一個(gè)進(jìn)程,而公司里的職員就是線程。一個(gè)公司至少得有一個(gè)職員吧,同理,一個(gè)進(jìn)程至少包含一個(gè)線程。在公司里,你可以一個(gè)職員干所有的事,但是效率很顯然是高不起來(lái)的,一個(gè)人的公司也不可能做大;一個(gè)程序中也可以只用一個(gè)線程去做事,事實(shí)上,一些過(guò)時(shí)的語(yǔ)言如fortune,basic都是如此,但是象一個(gè)人的公司一樣,效率很低,如果做大程序,效率更低——事實(shí)上現(xiàn)在幾乎沒(méi)有單線程的商業(yè)軟件。公司的職員越多,老板就得發(fā)越多的薪水給他們,還得耗費(fèi)大量精力去管理他們,協(xié)調(diào)他們之間的矛盾和利益;程序也是如此,線程越多耗費(fèi)的資源也越多,需要CPU時(shí)間去跟蹤線程,還得解決諸如死鎖,同步等問(wèn)題。總之,如果你不想你的公司被稱為“皮包公司”,你就得多幾個(gè)員工;如果你不想讓你的程序顯得稚氣,就在你的程序里引入多線程吧!?
本文將對(duì)C#編程中的多線程機(jī)制進(jìn)行探討,通過(guò)一些實(shí)例解決對(duì)線程的控制,多線程間通訊等問(wèn)題。為了省去創(chuàng)建GUI那些繁瑣的步驟,更清晰地逼近線程的本質(zhì),下面所有的程序都是控制臺(tái)程序,程序最后的Console.ReadLine()是為了使程序中途停下來(lái),以便看清楚執(zhí)行過(guò)程中的輸出。?
好了,廢話少說(shuō),讓我們來(lái)體驗(yàn)一下多線程的C#吧!?
二.操縱一個(gè)線程?
任何程序在執(zhí)行時(shí),至少有一個(gè)主線程,下面這段小程序可以給讀者一個(gè)直觀的印象:
編譯執(zhí)行后你看到了什么?是的,程序?qū)a(chǎn)生如下輸出:?
System Thread's Status:Running?
在這里,我們通過(guò)Thread類的靜態(tài)屬性CurrentThread獲取了當(dāng)前執(zhí)行的線程,對(duì)其Name屬性賦值“System Thread”,最后還輸出了它的當(dāng)前狀態(tài)(ThreadState)。所謂靜態(tài)屬性,就是這個(gè)類所有對(duì)象所公有的屬性,不管你創(chuàng)建了多少個(gè)這個(gè)類的實(shí)例,但是類的靜態(tài)屬性在內(nèi)存中只有一個(gè)。很容易理解CurrentThread為什么是靜態(tài)的——雖然有多個(gè)線程同時(shí)存在,但是在某一個(gè)時(shí)刻,CPU只能執(zhí)行其中一個(gè)。?
就像上面程序所演示的,我們通過(guò)Thread類來(lái)創(chuàng)建和控制線程。注意到程序的頭部,我們使用了如下命名空間:
1 |
下面我們就動(dòng)手來(lái)創(chuàng)建一個(gè)線程,使用Thread類創(chuàng)建線程時(shí),只需提供線程入口即可。線程入口使程序知道該讓這個(gè)線程干什么事,在C#中,線程入口是通過(guò)ThreadStart代理(delegate)來(lái)提供的,你可以把ThreadStart理解為一個(gè)函數(shù)指針,指向線程要執(zhí)行的函數(shù),當(dāng)調(diào)用Thread.Start()方法后,線程就開(kāi)始執(zhí)行ThreadStart所代表或者說(shuō)指向的函數(shù)。?
打開(kāi)你的VS.net,新建一個(gè)控制臺(tái)應(yīng)用程序(Console Application),下面這些代碼將讓你體味到完全控制一個(gè)線程的無(wú)窮樂(lè)趣!
這段程序包含兩個(gè)類Alpha和Simple,在創(chuàng)建線程oThread時(shí)我們用指向Alpha.Beta()方法的初始化了ThreadStart代理(delegate)對(duì)象,當(dāng)我們創(chuàng)建的線程oThread調(diào)用oThread.Start()方法啟動(dòng)時(shí),實(shí)際上程序運(yùn)行的是Alpha.Beta()方法:
Alpha oAlpha = new Alpha();Thread oThread = new Thread(new ThreadStart(oAlpha.Beta));oThread.Start();然后在Main()函數(shù)的while循環(huán)中,我們使用靜態(tài)方法Thread.Sleep()讓主線程停了1ms,這段時(shí)間CPU轉(zhuǎn)向執(zhí)行線程oThread。然后我們?cè)噲D用Thread.Abort()方法終止線程oThread,注意后面的oThread.Join(),Thread.Join()方法使主線程等待,直到oThread線程結(jié)束。你可以給Thread.Join()方法指定一個(gè)int型的參數(shù)作為等待的最長(zhǎng)時(shí)間。之后,我們?cè)噲D用Thread.Start()方法重新啟動(dòng)線程oThread,但是顯然Abort()方法帶來(lái)的后果是不可恢復(fù)的終止線程,所以最后程序會(huì)拋出ThreadStateException異常。?
程序最后得到的結(jié)果將如下圖:
?
在這里我們要注意的是其它線程都是依附于Main()函數(shù)所在的線程的,Main()函數(shù)是C#程序的入口,起始線程可以稱之為主線程,如果所有的前臺(tái)線程都停止了,那么主線程可以終止,而所有的后臺(tái)線程都將無(wú)條件終止。而所有的線程雖然在微觀上是串行執(zhí)行的,但是在宏觀上你完全可以認(rèn)為它們?cè)诓⑿袌?zhí)行。?
讀者一定注意到了Thread.ThreadState這個(gè)屬性,這個(gè)屬性代表了線程運(yùn)行時(shí)狀態(tài),在不同的情況下有不同的值,于是我們有時(shí)候可以通過(guò)對(duì)該值的判斷來(lái)設(shè)計(jì)程序流程。ThreadState在各種情況下的可能取值如下:
- Aborted:線程已停止
- AbortRequested:線程的Thread.Abort()方法已被調(diào)用,但是線程還未停止
- Background:線程在后臺(tái)執(zhí)行,與屬性Thread.IsBackground有關(guān)
- Running:線程正在正常運(yùn)行
- Stopped:線程已經(jīng)被停止
- StopRequested:線程正在被要求停止
- Suspended:線程已經(jīng)被掛起(此狀態(tài)下,可以通過(guò)調(diào)用Resume()方法重新運(yùn)行)
- SuspendRequested:線程正在要求被掛起,但是未來(lái)得及響應(yīng)
- Unstarted:未調(diào)用Thread.Start()開(kāi)始線程的運(yùn)行
- WaitSleepJoin:線程因?yàn)檎{(diào)用了Wait(),Sleep()或Join()等方法處于封鎖狀態(tài)
上面提到了Background狀態(tài)表示該線程在后臺(tái)運(yùn)行,那么后臺(tái)運(yùn)行的線程有什么特別的地方呢?其實(shí)后臺(tái)線程跟前臺(tái)線程只有一個(gè)區(qū)別,那就是后臺(tái)線程不妨礙程序的終止。一旦一個(gè)進(jìn)程所有的前臺(tái)線程都終止后,CLR(通用語(yǔ)言運(yùn)行環(huán)境)將通過(guò)調(diào)用任意一個(gè)存活中的后臺(tái)進(jìn)程的Abort()方法來(lái)徹底終止進(jìn)程。?
當(dāng)線程之間爭(zhēng)奪CPU時(shí)間時(shí),CPU按照是線程的優(yōu)先級(jí)給予服務(wù)的。在C#應(yīng)用程序中,用戶可以設(shè)定5個(gè)不同的優(yōu)先級(jí),由高到低分別是Highest,AboveNormal,Normal,BelowNormal,Lowest,在創(chuàng)建線程時(shí)如果不指定優(yōu)先級(jí),那么系統(tǒng)默認(rèn)為ThreadPriority.Normal。給一個(gè)線程指定優(yōu)先級(jí)
,我們可以使用如下代碼:
通過(guò)設(shè)定線程的優(yōu)先級(jí),我們可以安排一些相對(duì)重要的線程優(yōu)先執(zhí)行,例如對(duì)用戶的響應(yīng)等等。?
現(xiàn)在我們對(duì)怎樣創(chuàng)建和控制一個(gè)線程已經(jīng)有了一個(gè)初步的了解,下面我們將深入研究線程實(shí)現(xiàn)中比較典型的的問(wèn)題,并且探討其解決方法。?
三.線程的同步和通訊——生產(chǎn)者和消費(fèi)者?
假設(shè)這樣一種情況,兩個(gè)線程同時(shí)維護(hù)一個(gè)隊(duì)列,如果一個(gè)線程對(duì)隊(duì)列中添加元素,而另外一個(gè)線程從隊(duì)列中取用元素,那么我們稱添加元素的線程為生產(chǎn)者,稱取用元素的線程為消費(fèi)者。生產(chǎn)者與消費(fèi)者問(wèn)題看起來(lái)很簡(jiǎn)單,但是卻是多線程應(yīng)用中一個(gè)必須解決的問(wèn)題,它涉及到線程之間的同步和通訊問(wèn)題。?
前面說(shuō)過(guò),每個(gè)線程都有自己的資源,但是代碼區(qū)是共享的,即每個(gè)線程都可以執(zhí)行相同的函數(shù)。但是多線程環(huán)境下,可能帶來(lái)的問(wèn)題就是幾個(gè)線程同時(shí)執(zhí)行一個(gè)函數(shù),導(dǎo)致數(shù)據(jù)的混亂,產(chǎn)生不可預(yù)料的結(jié)果,因此我們必須避免這種情況的發(fā)生。C#提供了一個(gè)關(guān)鍵字lock,它可以把一段代碼定義為互斥段(critical section),互斥段在一個(gè)時(shí)刻內(nèi)只允許一個(gè)線程進(jìn)入執(zhí)行,而其他線程必須等待。在C#中,關(guān)鍵字lock定義如下:
expression代表你希望跟蹤的對(duì)象,通常是對(duì)象引用。一般地,如果你想保護(hù)一個(gè)類的實(shí)例,你可以使用this;如果你希望保護(hù)一個(gè)靜態(tài)變量(如互斥代碼段在一個(gè)靜態(tài)方法內(nèi)部),一般使用類名就可以了。而statement_block就是互斥段的代碼,這段代碼在一個(gè)時(shí)刻內(nèi)只可能被一個(gè)線程執(zhí)行。?
下面是一個(gè)使用lock關(guān)鍵字的典型例子,我將在注釋里向大家說(shuō)明lock關(guān)鍵字的用法和用途:
而多線程公用一個(gè)對(duì)象時(shí),也會(huì)出現(xiàn)和公用代碼類似的問(wèn)題,這種問(wèn)題就不應(yīng)該使用lock關(guān)鍵字了,這里需要用到System.Threading中的一個(gè)類Monitor,我們可以稱之為監(jiān)視器,Monitor提供了使線程共享資源的方案。?
Monitor類可以鎖定一個(gè)對(duì)象,一個(gè)線程只有得到這把鎖才可以對(duì)該對(duì)象進(jìn)行操作。對(duì)象鎖機(jī)制保證了在可能引起混亂的情況下一個(gè)時(shí)刻只有一個(gè)線程可以訪問(wèn)這個(gè)對(duì)象。Monitor必須和一個(gè)具體的對(duì)象相關(guān)聯(lián),但是由于它是一個(gè)靜態(tài)的類,所以不能使用它來(lái)定義對(duì)象,而且它的所有方法都是靜態(tài)的,不能使用對(duì)象來(lái)引用。下面代碼說(shuō)明了使用Monitor鎖定一個(gè)對(duì)象的情形:
如上所示,當(dāng)一個(gè)線程調(diào)用Monitor.Enter()方法鎖定一個(gè)對(duì)象時(shí),這個(gè)對(duì)象就歸它所有了,其它線程想要訪問(wèn)這個(gè)對(duì)象,只有等待它使用Monitor.Exit()方法釋放鎖。為了保證線程最終都能釋放鎖,你可以把Monitor.Exit()方法寫在try-catch-finally結(jié)構(gòu)中的finally代碼塊里。對(duì)于任何一個(gè)被Monitor鎖定的對(duì)象,內(nèi)存中都保存著與它相關(guān)的一些信息,其一是現(xiàn)在持有鎖的線程的引用,其二是一個(gè)預(yù)備隊(duì)列,隊(duì)列中保存了已經(jīng)準(zhǔn)備好獲取鎖的線程,其三是一個(gè)等待隊(duì)列,隊(duì)列中保存著當(dāng)前正在等待這個(gè)對(duì)象狀態(tài)改變的隊(duì)列的引用。當(dāng)擁有對(duì)象鎖的線程準(zhǔn)備釋放鎖時(shí),它使用Monitor.Pulse()方法通知等待隊(duì)列中的第一個(gè)線程,于是該線程被轉(zhuǎn)移到預(yù)備隊(duì)列中,當(dāng)對(duì)象鎖被釋放時(shí),在預(yù)備隊(duì)列中的線程可以立即獲得對(duì)象鎖。?
下面是一個(gè)展示如何使用lock關(guān)鍵字和Monitor類來(lái)實(shí)現(xiàn)線程的同步和通訊的例子,也是一個(gè)典型的生產(chǎn)者與消費(fèi)者問(wèn)題。這個(gè)例程中,生產(chǎn)者線程和消費(fèi)者線程是交替進(jìn)行的,生產(chǎn)者寫入一個(gè)數(shù),消費(fèi)者立即讀取并且顯示,我將在注釋中介紹該程序的精要所在。用到的系統(tǒng)命名空間如下:
首先,我們定義一個(gè)被操作的對(duì)象的類Cell,在這個(gè)類里,有兩個(gè)方法:ReadFromCell()和WriteToCell。消費(fèi)者線程將調(diào)用ReadFromCell()讀取cellContents的內(nèi)容并且顯示出來(lái),生產(chǎn)者進(jìn)程將調(diào)用WriteToCell()方法向cellContents寫入數(shù)據(jù)。
public class Cell{int cellContents; // Cell對(duì)象里邊的內(nèi)容bool readerFlag = false; // 狀態(tài)標(biāo)志,為true時(shí)可以讀取,為false則正在寫入public int ReadFromCell( ){lock(this) // Lock關(guān)鍵字保證了什么,請(qǐng)大家看前面對(duì)lock的介紹 {if (!readerFlag)//如果現(xiàn)在不可讀取 { try{file://等待WriteToCell方法中調(diào)用Monitor.Pulse()方法Monitor.Wait(this);}catch (SynchronizationLockException e){Console.WriteLine(e);}catch (ThreadInterruptedException e){Console.WriteLine(e);}}Console.WriteLine("Consume: {0}",cellContents);readerFlag = false; file://重置readerFlag標(biāo)志,表示消費(fèi)行為已經(jīng)完成Monitor.Pulse(this); file://通知WriteToCell()方法(該方法在另外一個(gè)線程中執(zhí)行,等待中) }return cellContents;}public void WriteToCell(int n){lock(this){if (readerFlag){try{Monitor.Wait(this);}catch (SynchronizationLockException e){file://當(dāng)同步方法(指Monitor類除Enter之外的方法)在非同步的代碼區(qū)被調(diào)用 Console.WriteLine(e);}catch (ThreadInterruptedException e){file://當(dāng)線程在等待狀態(tài)的時(shí)候中止 Console.WriteLine(e);}}cellContents = n;Console.WriteLine("Produce: {0}",cellContents);readerFlag = true; Monitor.Pulse(this); file://通知另外一個(gè)線程中正在等待的ReadFromCell()方法 }}}下面定義生產(chǎn)者CellProd和消費(fèi)者類CellCons,它們都只有一個(gè)方法ThreadRun(),以便在Main()函數(shù)中提供給線程的ThreadStart代理對(duì)象,作為線程的入口。
public class CellProd{Cell cell; // 被操作的Cell對(duì)象int quantity = 1; // 生產(chǎn)者生產(chǎn)次數(shù),初始化為1 public CellProd(Cell box, int request){//構(gòu)造函數(shù)cell = box; quantity = request; }public void ThreadRun( ){for(int looper=1; looper<=quantity; looper++)cell.WriteToCell(looper); file://生產(chǎn)者向操作對(duì)象寫入信息 }} public class CellCons{Cell cell; int quantity = 1; public CellCons(Cell box, int request){cell = box; quantity = request; }public void ThreadRun( ){int valReturned;for(int looper=1; looper<=quantity; looper++)valReturned=cell.ReadFromCell( );//消費(fèi)者從操作對(duì)象中讀取信息 }}然后在下面這個(gè)類MonitorSample的Main()函數(shù)中我們要做的就是創(chuàng)建兩個(gè)線程分別作為生產(chǎn)者和消費(fèi)者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法對(duì)同一個(gè)Cell對(duì)象進(jìn)行操作。
public class MonitorSample{public static void Main(String[] args){int result = 0; file://一個(gè)標(biāo)志位,如果是0表示程序沒(méi)有出錯(cuò),如果是1表明有錯(cuò)誤發(fā)生Cell cell = new Cell( ); //下面使用cell初始化CellProd和CellCons兩個(gè)類,生產(chǎn)和消費(fèi)次數(shù)均為20次CellProd prod = new CellProd(cell, 20); CellCons cons = new CellCons(cell, 20); Thread producer = new Thread(new ThreadStart(prod.ThreadRun));Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));//生產(chǎn)者線程和消費(fèi)者線程都已經(jīng)被創(chuàng)建,但是沒(méi)有開(kāi)始執(zhí)行 try{producer.Start( );consumer.Start( ); producer.Join( ); consumer.Join( );Console.ReadLine();}catch (ThreadStateException e){file://當(dāng)線程因?yàn)樗帬顟B(tài)的原因而不能執(zhí)行被請(qǐng)求的操作 Console.WriteLine(e); result = 1; }catch (ThreadInterruptedException e){file://當(dāng)線程在等待狀態(tài)的時(shí)候中止 Console.WriteLine(e); result = 1; }//盡管Main()函數(shù)沒(méi)有返回值,但下面這條語(yǔ)句可以向父進(jìn)程返回執(zhí)行結(jié)果Environment.ExitCode = result;}}大家可以看到,在上面的例程中,同步是通過(guò)等待Monitor.Pulse()來(lái)完成的。首先生產(chǎn)者生產(chǎn)了一個(gè)值,而同一時(shí)刻消費(fèi)者處于等待狀態(tài),直到收到生產(chǎn)者的“脈沖(Pulse)”通知它生產(chǎn)已經(jīng)完成,此后消費(fèi)者進(jìn)入消費(fèi)狀態(tài),而生產(chǎn)者開(kāi)始等待消費(fèi)者完成操作后將調(diào)用Monitor.Pulese()發(fā)出的“脈沖”。它的執(zhí)行結(jié)果很簡(jiǎn)單:
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20
事實(shí)上,這個(gè)簡(jiǎn)單的例子已經(jīng)幫助我們解決了多線程應(yīng)用程序中可能出現(xiàn)的大問(wèn)題,只要領(lǐng)悟了解決線程間沖突的基本方法,很容易把它應(yīng)用到比較復(fù)雜的程序中去。
?
關(guān)于JOIN()的一點(diǎn)理解
?? ? Thread.Join()在MSDN中的解釋很模糊:Blocks the calling thread until a thread terminates
有兩個(gè)主要問(wèn)題:1.什么是the calling thread?
? ? ? ? ? ? ? ? ? ???? 2.什么是a thread??
?
? ? ?? 首先來(lái)看一下有關(guān)的概念: 我們執(zhí)行一個(gè).exe文件實(shí)際上就是開(kāi)啟了一個(gè)進(jìn)程,同時(shí)開(kāi)啟了至少一個(gè)線程,
但是真正干活的是線程,就好比一個(gè)Team有好幾個(gè)人,但是真正干活的是人不是Team.?
????? 具體到代碼來(lái)說(shuō),以Console Application為例:程序Test.exe從Main函數(shù)開(kāi)始運(yùn)行,實(shí)際上是有一個(gè)線程
在執(zhí)行Main函數(shù),我們稱作MainThread.假如我們?cè)贛ain函數(shù)中聲明了一個(gè)Thread,稱作NewThread,并且調(diào)用了
NewThread.Start()的方法,那么 MainThread在處理Main函數(shù)里面的代碼時(shí)遇到NewThread.Start()時(shí),就會(huì)
去調(diào)用NewThread.
? ? ?? 基于上面的討論,我們可以得出結(jié)論:在我們剛才的例子中the calling thread就是MainThread,而a thread
指的洽洽就是MainThread調(diào)用的NewThread線程。?
?????? 現(xiàn)在回到MSDN的解釋,我們可以這么翻譯:當(dāng)NewThread調(diào)用Join方法的時(shí)候,MainThread就被停止執(zhí)行,
直到NewThread線程執(zhí)行完畢。這樣就好理解了吧O(∩_∩)O哈哈~?
?
? ? ?? 好了,前面分析完了,現(xiàn)在來(lái)看測(cè)試用例吧:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading;namespace Test {class TestThread{private static void ThreadFuncOne(){for (int i = 0; i < 10; i++){Console.WriteLine(Thread.CurrentThread.Name +" i = " + i);}Console.WriteLine(Thread.CurrentThread.Name + " has finished");}static void Main(string[] args){Thread.CurrentThread.Name = "MainThread";Thread newThread = new Thread(new ThreadStart(TestThread.ThreadFuncOne));newThread.Name = "NewThread";for (int j = 0; j < 20; j++){if (j == 10){newThread.Start();newThread.Join();}else{Console.WriteLine(Thread.CurrentThread.Name + " j = " + j);}}Console.Read();}} }下面是測(cè)試的結(jié)果:
?
結(jié)論:從測(cè)試中我們可以很清楚的看到MainThread在NewThread.Join被調(diào)用后被阻塞,直到NewThread
執(zhí)行完畢才繼續(xù)執(zhí)行。
?
關(guān)于 Monitor.Wait()和Pulse()的知識(shí)
1.Monitor.Wait方法
當(dāng)線程調(diào)用 Wait 時(shí),它釋放對(duì)象的鎖并進(jìn)入對(duì)象的等待隊(duì)列,對(duì)象的就緒隊(duì)列中的下一個(gè)線程(如果有)獲取鎖并擁有對(duì)對(duì)象的獨(dú)占使用。Wait()就是交出鎖的使用權(quán),使線程處于阻塞狀態(tài),直到再次獲得鎖的使用權(quán)。
2.Monitor.Pulse方法
當(dāng)前線程調(diào)用此方法以便向隊(duì)列中的下一個(gè)線程發(fā)出鎖的信號(hào)。接收到脈沖后,等待線程就被移動(dòng)到就緒隊(duì)列中。在調(diào)用 Pulse 的線程釋放鎖后,就緒隊(duì)列中的下一個(gè)線程(不一定是接收到脈沖的線程)將獲得該鎖。pulse()并不會(huì)使當(dāng)前線程釋放鎖。
?
簡(jiǎn)述:
共用同一lock對(duì)象兩線程不能只調(diào)用Wait(),Wait這個(gè)方法反而放棄了鎖的使用權(quán),同時(shí)阻塞當(dāng)前線程,線程就直接休眠(進(jìn)入WaitSleepJoin狀態(tài)),同時(shí)在主線程中Join這個(gè)work線程時(shí),也就一直不能返回了。線程將一直阻塞。
讓我們首先看看MSDN對(duì)Monitor.Wait的解釋(鏈接見(jiàn)注釋):
釋放對(duì)象上的鎖并阻止當(dāng)前線程,直到它重新獲取該鎖。...
該解釋的確很粗糙,很難理解。讓我們來(lái)看看它下面的備注:
同步的對(duì)象包含若干引用,其中包括對(duì)當(dāng)前擁有鎖的線程的引用、對(duì)就緒隊(duì)列的引用和對(duì)等待隊(duì)列的引用。
這個(gè)多少還給了點(diǎn)東西,現(xiàn)在我們腦海中想像這么一幅圖畫(huà):
當(dāng)一個(gè)線程嘗試著lock一個(gè)同步對(duì)象的時(shí)候,該線程就在就緒隊(duì)列中排隊(duì)。一旦沒(méi)人擁有該同步對(duì)象,就緒隊(duì)列中的線程就可以占有該同步對(duì)象。這也是我們平時(shí)最經(jīng)常用的lock方法。
為了其他的同步目的,占有同步對(duì)象的線程也可以暫時(shí)放棄同步對(duì)象,并把自己流放到等待隊(duì)列中去。這就是Monitor.Wait。由于該線程放棄了同步對(duì)象,其他在就緒隊(duì)列的排隊(duì)者就可以進(jìn)而擁有同步對(duì)象。
比起就緒隊(duì)列來(lái)說(shuō),在等待隊(duì)列中排隊(duì)的線程更像是二等公民:他們不能自動(dòng)得到同步對(duì)象,甚至不能自動(dòng)升艙到就緒隊(duì)列。而Monitor.Pulse的作用就是開(kāi)一次門,使得一個(gè)正在等待隊(duì)列中的線程升艙到就緒隊(duì)列;相應(yīng)的Monitor.PulseAll則打開(kāi)門放所有等待隊(duì)列中的線程到就緒隊(duì)列。
比如下面的程序:
?
從時(shí)間線上來(lái)分析:
Assembly code T 線程A ---0lock( lockObj ) ---1 { //... 線程B 線程C ---2//... lock( lockObj ) lock( lockObj ) ---3//... { { ---4//... //... ---5//... //... ---6Monitor.Pulse //... ---7Monitor.Wait //... ---8//... Monitor.Pulse ---9//... } } ---10 } 時(shí)間點(diǎn)0,假設(shè)線程A先得到了同步對(duì)象,它就登記到同步對(duì)象lockObj的“擁有者引用”中。時(shí)間點(diǎn)3,線程B和C要求擁有同步對(duì)象,他們將在“就緒隊(duì)列”排隊(duì): |--(擁有鎖的線程) A | 3 lockObj--|--(就緒隊(duì)列) B,C | |--(等待隊(duì)列) 時(shí)間點(diǎn)7,線程A用Pulse發(fā)出信號(hào),允許第一個(gè)正在"等待隊(duì)列"中的線程進(jìn)入到”就緒隊(duì)列“。但由于就緒隊(duì)列是空的,什么事也沒(méi)有發(fā)生。時(shí)間點(diǎn)8,線程A用Wait放棄同步對(duì)象,并把自己放入"等待隊(duì)列"。B,C已經(jīng)在就緒隊(duì)列中,因此其中的一個(gè)得以獲得同步對(duì)象(假定是B)。B成了同步 對(duì)象的擁有者。C現(xiàn)在還是候補(bǔ)委員,可以自動(dòng)獲得空缺。而A則被關(guān)在門外,不能自動(dòng)獲得空缺。 |--(擁有鎖的線程) B | 8 lockObj--|--(就緒隊(duì)列) C | |--(等待隊(duì)列) A 時(shí)間點(diǎn)9,線程B用Pulse發(fā)出信號(hào)開(kāi)門,第一個(gè)被關(guān)在門外的A被允許放入到就緒隊(duì)列,現(xiàn)在C和A都成了候補(bǔ)委員,一旦同步對(duì)象空閑,都有機(jī)會(huì)得它。 |--(擁有鎖的線程) B | 9 lockObj--|--(就緒隊(duì)列) C,A | |--(等待隊(duì)列) 時(shí)間點(diǎn)10,線程B退出Lock區(qū)塊,同步對(duì)象閑置,就緒隊(duì)列隊(duì)列中的C或A就可以轉(zhuǎn)正為擁有者(假設(shè)C得到了同步對(duì)象)。 |--(擁有鎖的線程) C | 10 lockObj--|--(就緒隊(duì)列) A | |--(等待隊(duì)列) 隨后C也退出Lock區(qū)塊,同步對(duì)象閑置,A就重新得到了同步對(duì)象,并從Monitor.Wait中返回... 最終的執(zhí)行結(jié)果就是: B exit... C exit... A exit...?
由于Monitor.Wait的暫時(shí)放棄和Monitor.Pulse的開(kāi)門機(jī)制,我們可以用Monitor來(lái)實(shí)現(xiàn)更豐富的同步機(jī)制,比如一個(gè)事件機(jī)(ManualResetEvent):
C# code class MyManualEvent{
privateobject lockObj =newobject();
privatebool hasSet =false;
publicvoid Set()
{
lock (lockObj)
{
hasSet =true;
Monitor.PulseAll(lockObj);
}
}
publicvoid WaitOne()
{
lock (lockObj)
{
while (!hasSet)
{
Monitor.Wait(lockObj);
}
}
}
}
class Program
{
static MyManualEvent myManualEvent =new MyManualEvent();
staticvoid Main(string[] args)
{
ThreadPool.QueueUserWorkItem(WorkerThread, "A");
ThreadPool.QueueUserWorkItem(WorkerThread, "B");
Console.WriteLine("Press enter to signal the green light");
Console.ReadLine();
myManualEvent.Set(); ThreadPool.QueueUserWorkItem(WorkerThread, "C");
Console.ReadLine();
}
staticvoid WorkerThread(object state)
{
myManualEvent.WaitOne();
Console.WriteLine("Thread {0} got the green light...", state);
}
}
我們看到了該玩具M(jìn)yManualEvent實(shí)現(xiàn)了類庫(kù)中的ManulaResetEvent的功能,但卻更加的輕便 - 類庫(kù)的ManulaResetEvent使用了操作系統(tǒng)內(nèi)核事件機(jī)制,負(fù)擔(dān)比較大(不算競(jìng)態(tài)時(shí)間,ManulaResetEvent是微秒級(jí),而lock是幾十納秒級(jí))。
例子的WaitOne中先在lock的保護(hù)下判斷是否信號(hào)綠燈,如果不是則進(jìn)入等待。因此可以有多個(gè)線程(比如例子中的AB)在等待隊(duì)列中排隊(duì)。
當(dāng)調(diào)用Set的時(shí)候,在lock的保護(hù)下信號(hào)轉(zhuǎn)綠,并使用PulseAll開(kāi)門放狗,將所有排在等待隊(duì)列中的線程放入就緒隊(duì)列,A或B(比如A)于是可以重新獲得同步對(duì)象,從Monitor.Wait退出,并隨即退出lock區(qū)塊,WaitOne返回。隨后B或A(比如B)重復(fù)相同故事,并從WaitOne返回。
線程C在myManualEvent.Set()后才執(zhí)行,它在WaitOne中確信信號(hào)燈早已轉(zhuǎn)綠,于是可以立刻返回并得以執(zhí)行隨后的命令。
該玩具M(jìn)yManualEvent可以用在需要等待初始化的場(chǎng)合,比如多個(gè)工作線程都必須等到初始化完成后,接到OK信號(hào)后才能開(kāi)工。該玩具M(jìn)yManualEvent比起ManulaResetEvent有很多局限,比如不能跨進(jìn)程使用,但它演示了通過(guò)基本的Monitor命令組合,達(dá)到事件機(jī)的作用。
現(xiàn)在是回答朋友們的疑問(wèn)的時(shí)候了:
Q:?Lock關(guān)鍵字不是有獲取鎖、釋放鎖的功能... 為什么還需要執(zhí)行Pulse?
A: 因?yàn)閃ait和Pulse另有用途。
Q:?用lock 就不要用monitor了(?)
A: lock只是Monitor.Enter和Monitor.Exit,用Monitor的方法,不僅能用Wait,還可以用帶超時(shí)的Monitor.Enter重載。
Q:?Monitor.Wait完全沒(méi)必要 (?)
A: Wait和Pulse另有用途。
Q:?什么Pulse和Wait方法必須從同步的代碼塊內(nèi)調(diào)用?
A: 因?yàn)閃ait的本意就是“[暫時(shí)]釋放對(duì)象上的鎖并阻止當(dāng)前線程,直到它重新獲取該鎖”,沒(méi)有獲得就談不到釋放。
我們知道lock實(shí)際上一個(gè)語(yǔ)法糖糖,C#編譯器實(shí)際上把他展開(kāi)為Monitor.Enter和Monitor.Exit,即:
但是,這種實(shí)現(xiàn)邏輯至少理論上有一個(gè)錯(cuò)誤:當(dāng)Monitor.Enter(lockObj);剛剛完成,還沒(méi)有進(jìn)入try區(qū)的時(shí)候,有可能從其他線程發(fā)出了Thread.Abort等命令,使得該線程沒(méi)有機(jī)會(huì)進(jìn)入try...finally。也就是說(shuō)lockObj沒(méi)有辦法得到釋放,有可能造成程序死鎖。這也是Thread.Abort一般被認(rèn)為是邪惡的原因之一。
DotNet4開(kāi)始,增加了Monitor.Enter(object,ref bool)重載。而C#編譯器會(huì)把lock展開(kāi)為更安全的Monitor.Enter(object,ref bool)和Monitor.Exit:
現(xiàn)在Monitor.TryEnter在try的保護(hù)下,“加鎖”成功意味著“放鎖”將得到finally的保護(hù)。
轉(zhuǎn)載于:https://www.cnblogs.com/StupidsCat/archive/2013/01/05/2845505.html
總結(jié)
以上是生活随笔為你收集整理的C# 多线程控制 通讯 和切换的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 解题报告 百进制数
- 下一篇: C# 格式化字符串 String.For