久久久久无码精品,四川省少妇一级毛片,老老熟妇xxxxhd,人妻无码少妇一区二区

進(jìn)程和線程的區(qū)別

時(shí)間:2020-12-12 09:29:37 常識(shí) 我要投稿

進(jìn)程和線程的區(qū)別

  在學(xué)習(xí)編程時(shí),通常初學(xué)者會(huì)分不清楚進(jìn)程和線程,其實(shí)弄清楚者兩個(gè)概念并不是很難,下面就跟YJBYS小編一起來(lái)看看吧。

  簡(jiǎn)而言之,一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程.

  線程的劃分尺度小于進(jìn)程,使得多線程程序的并發(fā)性高。

  另外,進(jìn)程在執(zhí)行過(guò)程中擁有獨(dú)立的內(nèi)存單元,而多個(gè)線程共享內(nèi)存,從而極大地提高了程序的運(yùn)行效率。

  線程在執(zhí)行過(guò)程中與進(jìn)程還是有區(qū)別的。每個(gè)獨(dú)立的線程有一個(gè)程序運(yùn)行的入口、順序執(zhí)行序列和程序的出口。但是線程不能夠獨(dú)立執(zhí)行,必須依存在應(yīng)用程序中,由應(yīng)用程序提供多個(gè)線程執(zhí)行控制。

  從邏輯角度來(lái)看,多線程的意義在于一個(gè)應(yīng)用程序中,有多個(gè)執(zhí)行部分可以同時(shí)執(zhí)行。但操作系統(tǒng)并沒(méi)有將多個(gè)線程看做多個(gè)獨(dú)立的應(yīng)用,來(lái)實(shí)現(xiàn)進(jìn)程的調(diào)度和管理以及資源分配。這就是進(jìn)程和線程的重要區(qū)別。

  進(jìn)程是具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動(dòng),進(jìn)程是系統(tǒng)進(jìn)行資源分配和調(diào)度的一個(gè)獨(dú)立單位.

  線程是進(jìn)程的一個(gè)實(shí)體,是CPU調(diào)度和分派的基本單位,它是比進(jìn)程更小的能獨(dú)立運(yùn)行的基本單位.線程自己基本上不擁有系統(tǒng)資源,只擁有一點(diǎn)在運(yùn)行中必不可少的資源(如程序計(jì)數(shù)器,一組寄存器和棧),但是它可與同屬一個(gè)進(jìn)程的其他的線程共享進(jìn)程所擁有的全部資源.

  一個(gè)線程可以創(chuàng)建和撤銷另一個(gè)線程;同一個(gè)進(jìn)程中的多個(gè)線程之間可以并發(fā)執(zhí)行.

  進(jìn)程和線程的主要差別在于它們是不同的操作系統(tǒng)資源管理方式。進(jìn)程有獨(dú)立的地址空間,一個(gè)進(jìn)程崩潰后,在保護(hù)模式下不會(huì)對(duì)其它進(jìn)程產(chǎn)生影響,而線程只是一個(gè)進(jìn)程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程之間沒(méi)有單獨(dú)的地址空間,一個(gè)線程死掉就等于整個(gè)進(jìn)程死掉,所以多進(jìn)程的程序要比多線程的程序健壯,但在進(jìn)程切換時(shí),耗費(fèi)資源較大,效率要差一些。但對(duì)于一些要求同時(shí)進(jìn)行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進(jìn)程。如果有興趣深入的話,我建議你們看看《現(xiàn)代操作系統(tǒng)》或者《操作系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)》。對(duì)就個(gè)問(wèn)題說(shuō)得比較清楚。

  5.1 簡(jiǎn)介

  進(jìn)程(process)是一塊包含了某些資源的內(nèi)存區(qū)域。操作系統(tǒng)利用進(jìn)程把它的工作劃分為一些功能單元。

  進(jìn)程中所包含的一個(gè)或多個(gè)執(zhí)行單元稱為線程(thread)。進(jìn)程還擁有一個(gè)私有的虛擬地址空間,該空間僅能被它所包含的線程訪問(wèn)。

  當(dāng)運(yùn)行.NET程序時(shí),進(jìn)程還會(huì)把被稱為CLR的軟件層包含到它的內(nèi)存空間中。上一章曾經(jīng)對(duì)CLR做了詳細(xì)描述。該軟件層是在進(jìn)程創(chuàng)建期間由運(yùn)行時(shí)宿主載入的(參見(jiàn)4.2.3節(jié))。

  線程只能歸屬于一個(gè)進(jìn)程并且它只能訪問(wèn)該進(jìn)程所擁有的資源。當(dāng)操作系統(tǒng)創(chuàng)建一個(gè)進(jìn)程后,該進(jìn)程會(huì)自動(dòng)申請(qǐng)一個(gè)名為主線程或首要線程的線程。主線程將執(zhí)行運(yùn)行時(shí)宿主, 而運(yùn)行時(shí)宿主會(huì)負(fù)責(zé)載入CLR。

  應(yīng)用程序(application)是由一個(gè)或多個(gè)相互協(xié)作的進(jìn)程組成的。例如,Visual Studio開(kāi)發(fā)環(huán)境就是利用一個(gè)進(jìn)程編輯源文件,并利用另一個(gè)進(jìn)程完成編譯工作的應(yīng)用程序。

  在Windows NT/2000/XP操作系統(tǒng)下,我們可以通過(guò)任務(wù)管理器在任意時(shí)間查看所有的應(yīng)用程序和進(jìn)程。盡管只打開(kāi)了幾個(gè)應(yīng)用程序,但是通常情況下將有大約30個(gè)進(jìn)程同時(shí)運(yùn)行。 事實(shí)上,為了管理當(dāng)前的會(huì)話和任務(wù)欄以及其他一些任務(wù),系統(tǒng)執(zhí)行了大量的進(jìn)程。

  5.2 進(jìn)程

  5.2.1 簡(jiǎn)介

  在運(yùn)行于32位處理器上的32位Windows操作系統(tǒng)中,可將一個(gè)進(jìn)程視為一段大小為4GB(232字節(jié))的線性內(nèi)存空間,它起始于0x00000000結(jié)束于0xFFFFFFFF。這段內(nèi)存空間不能被其他進(jìn)程所訪問(wèn),所以稱為該進(jìn)程的私有空間。這段空間被平分為兩塊,2GB被系統(tǒng)所有,剩下2GB被用戶所有。

  如果有N個(gè)進(jìn)程運(yùn)行在同一臺(tái)機(jī)器上,那么將需要N×4GB的海量RAM,還好事實(shí)并非如此。

  Windows是按需為每個(gè)進(jìn)程分配內(nèi)存的,4GB是32位系統(tǒng)中一個(gè)進(jìn)程所占空間的上限。

  將進(jìn)程所需的內(nèi)存劃分為4KB大小的內(nèi)存頁(yè),并根據(jù)使用情況將這些內(nèi)存頁(yè)存儲(chǔ)在硬盤(pán)上或加載到RAM中,通過(guò)系統(tǒng)的這種虛擬內(nèi)存機(jī)制,我們可以有效地減少對(duì)實(shí)際內(nèi)存的需求量。當(dāng)然這些對(duì)用戶和開(kāi)發(fā)者來(lái)說(shuō)都是透明的。

  5.2.2 System.Diagnostics.Process類

  System.Diagnostics.Process類的實(shí)例可以引用一個(gè)進(jìn)程,被引用的進(jìn)程包含以下幾種。

  該實(shí)例的當(dāng)前進(jìn)程。

  本機(jī)上除了當(dāng)前進(jìn)程的其他進(jìn)程。

  遠(yuǎn)程機(jī)器上的某個(gè)進(jìn)程。

  通過(guò)該類所包含的方法和字段,可以創(chuàng)建或銷毀一個(gè)進(jìn)程,并且可以獲得一個(gè)進(jìn)程的相關(guān)信息。下面將討論一些使用該類實(shí)現(xiàn)的常見(jiàn)任務(wù)。

  5.2.3 創(chuàng)建和銷毀子進(jìn)程

  下面的程序創(chuàng)建了一個(gè)稱為子進(jìn)程的新進(jìn)程。在這種情況下,初始的進(jìn)程稱為父進(jìn)程。子進(jìn)程啟動(dòng)了一個(gè)記事本應(yīng)用程序。父進(jìn)程的線程在等待1秒后銷毀該子進(jìn)程。該程序的執(zhí)行效果就是打開(kāi)并關(guān)閉記事本。

  例5-1

  靜態(tài)方法Start()可以使用已存在的Windows文件擴(kuò)展名關(guān)聯(lián)機(jī)制。例如,我們可以利用下面的代碼執(zhí)行同樣的操作。

  默認(rèn)情況下,子進(jìn)程將繼承其父進(jìn)程的安全上下文。但還可以使用Process.Start()方法的一個(gè)重載版本在任意用戶的安全上下文中啟動(dòng)該子進(jìn)程,當(dāng)然需要通過(guò)一個(gè)System.Diagnostics. ProcessStartInfo類的實(shí)例來(lái)提供該用戶的用戶名和密碼。

  5.2.4 避免在一臺(tái)機(jī)器上同時(shí)運(yùn)行同一應(yīng)用程序的多個(gè)實(shí)例

  有些應(yīng)用程序需要這種功能。實(shí)際上,通常來(lái)說(shuō)在同一臺(tái)機(jī)器上同時(shí)運(yùn)行一個(gè)應(yīng)用程序的多個(gè)實(shí)例并沒(méi)有意義。

  直到現(xiàn)在,為了在Windows下滿足上述約束,開(kāi)發(fā)者最常用的方法仍然是使用有名互斥體(named mutex)技術(shù)(參見(jiàn)5.7.2節(jié))。然而采用這種技術(shù)來(lái)滿足上述約束存在以下缺點(diǎn):

  該技術(shù)具有使互斥體的名字被其他應(yīng)用程序所使用的較小的、潛在的風(fēng)險(xiǎn)。在這種情況下該技術(shù)將不再有效并且會(huì)造成很難檢測(cè)到的bug。

  該技術(shù)不能解決我們僅允許一個(gè)應(yīng)用程序產(chǎn)生N個(gè)實(shí)例這種一般的問(wèn)題。

  幸而在System.Diagnostics.Process類中擁有GetCurrentProcess()(返回當(dāng)前進(jìn)程)和GetPro- cesses()(返回機(jī)器上所有的進(jìn)程)這樣的靜態(tài)方法。在下面的程序中我們?yōu)樯鲜鰡?wèn)題找到了一個(gè)優(yōu)雅且簡(jiǎn)單的解決方案。

  例5-2

  通過(guò)方法參數(shù)指定了遠(yuǎn)程機(jī)器的名字后,GetProcesses()方法也可以返回遠(yuǎn)程機(jī)器上所有的進(jìn)程。

  5.2.5 終止當(dāng)前進(jìn)程

  可以調(diào)用System.Environment類中的靜態(tài)方法Exit(int exitCode)或FailFast(stringmessage)終止當(dāng)前進(jìn)程。Exit()方法是最好的選擇,它將徹底終止進(jìn)程并向操作系統(tǒng)返回指定的退出代碼值。之所以稱為徹底終止是因?yàn)楫?dāng)前對(duì)象的所有清理工作以及finally塊的執(zhí)行都將由不同的線程完成。當(dāng)然,終止進(jìn)程將花費(fèi)一定的時(shí)間。

  顧名思義,F(xiàn)ailFast()方法可以迅速終止進(jìn)程。Exit()方法所做的預(yù)防措施將被它忽略。只有一個(gè)包含了指定信息的嚴(yán)重錯(cuò)誤會(huì)操作系統(tǒng)記錄到日志中。你可能想要在探查問(wèn)題的時(shí)候使用該方法,因?yàn)榭梢詫⒃摮绦虻膹氐捉K止視為數(shù)據(jù)惡化的起因。

  5.3 線程

  5.3.1 簡(jiǎn)介

  一個(gè)線程包含以下內(nèi)容。

  一個(gè)指向當(dāng)前被執(zhí)行指令的指令指針;

  一個(gè)棧;

  一個(gè)寄存器值的集合,定義了一部分描述正在執(zhí)行線程的處理器狀態(tài)的.值;

  一個(gè)私有的數(shù)據(jù)區(qū)。

  所有這些元素都?xì)w于線程執(zhí)行上下文的名下。處在同一個(gè)進(jìn)程中的所有線程都可以訪問(wèn)該進(jìn)程所包含的地址空間,當(dāng)然也包含存儲(chǔ)在該空間中的所有資源。

  我們不準(zhǔn)備討論線程在內(nèi)核模式或者用戶模式執(zhí)行的問(wèn)題。盡管.NET以前的Windows一直使用這兩種模式,并且依然存在,但是對(duì).NET Framework來(lái)說(shuō)它們是不可見(jiàn)的。

  并行使用一些線程通常是我們?cè)趯?shí)現(xiàn)算法時(shí)的自然反應(yīng)。實(shí)際上,一個(gè)算法往往由一系列可以并發(fā)執(zhí)行的任務(wù)組成。但是需要引起注意的是,使用大量的線程將引起過(guò)多的上下文切換,最終反而影響了性能。

  同樣,幾年前我們就注意到,預(yù)測(cè)每18個(gè)月處理器運(yùn)算速度增加一倍的摩爾定律已不再成立。處理器的頻率停滯在3GHz~4GHz上下。這是由于物理上的限制,需要一段時(shí)間才能取得突破。同時(shí),為了在性能競(jìng)爭(zhēng)中不會(huì)落敗,較大的處理器制造商如AMD和Intel目前都將目標(biāo)轉(zhuǎn)向多核芯片。因此我們可以預(yù)計(jì)在接下去的幾年中這種類型的架構(gòu)將廣泛被采用。在這種情況下,改進(jìn)應(yīng)用性能的唯一方案就是合理地利用多線程技術(shù)。

  5.3.2 受托管的線程與 Windows線程

  必須要了解,執(zhí)行.NET應(yīng)用的線程實(shí)際上仍然是Windows線程。但是,當(dāng)某個(gè)線程被CLR所知時(shí),我們將它稱為受托管的線程。具體來(lái)說(shuō),由受托管的代碼創(chuàng)建出來(lái)的線程就是受托管的線程。如果一個(gè)線程由非托管的代碼所創(chuàng)建,那么它就是非托管的線程。不過(guò),一旦該線程執(zhí)行了受托管的代碼它就變成了受托管的線程。

  一個(gè)受托管的線程和非托管的線程的區(qū)別在于,CLR將創(chuàng)建一個(gè)System.Threading.Thread類的實(shí)例來(lái)代表并操作前者。在內(nèi)部實(shí)現(xiàn)中,CLR將一個(gè)包含了所有受托管線程的列表保存在一個(gè)叫做ThreadStore地方。

  CLR確保每一個(gè)受托管的線程在任意時(shí)刻都在一個(gè)AppDomain中執(zhí)行,但是這并不代表一個(gè)線程將永遠(yuǎn)處在一個(gè)AppDomain中,它可以隨著時(shí)間的推移轉(zhuǎn)到其他的AppDomain中。關(guān)于AppDomain的概念參見(jiàn)4.1。

  從安全的角度來(lái)看,一個(gè)受托管的線程的主用戶與底層的非托管線程中的Windows主用戶是無(wú)關(guān)的。

  5.3.3 搶占式多任務(wù)處理

  我們可以問(wèn)自己下面這個(gè)問(wèn)題: 我的計(jì)算機(jī)只有一個(gè)處理器,然而在任務(wù)管理器中我們卻可以看到數(shù)以百計(jì)的線程正同時(shí)運(yùn)行在機(jī)器上!這怎么可能呢?

  多虧了搶占式多任務(wù)處理,通過(guò)它對(duì)線程的調(diào)度,使得上述問(wèn)題成為可能。調(diào)度器作為Windows內(nèi)核的一部分,將時(shí)間切片,分成一段段的時(shí)間片。這些時(shí)間間隔以毫秒為精度且長(zhǎng)度并不固定。針對(duì)每個(gè)處理器,每個(gè)時(shí)間片僅服務(wù)于單獨(dú)一個(gè)線程。線程的迅速執(zhí)行給我們?cè)斐闪怂鼈冊(cè)谕瑫r(shí)運(yùn)行的假象。我們?cè)趦蓚(gè)時(shí)間片的間隔中進(jìn)行上下文切換。該方法的優(yōu)點(diǎn)在于,那些正在等待某些Windows資源的線程將不會(huì)浪費(fèi)時(shí)間片,直到資源有效為止。

  之所以用搶占式這個(gè)形容詞來(lái)修飾這種多任務(wù)管理方式,是因?yàn)樵诖朔N方式下線程將被系統(tǒng)強(qiáng)制性中斷。那些對(duì)此比較好奇的人應(yīng)該了解到,在上下文切換的過(guò)程中,操作系統(tǒng)會(huì)在下一個(gè)線程將要執(zhí)行的代碼中插入一條跳轉(zhuǎn)到下一個(gè)上下文切換的指令。該指令是一個(gè)軟中斷,如果線程在遇到這條指令前就終止了(例如,它正在等待某個(gè)資源),那么該指定將被刪除而上下文切換也將提前發(fā)生。

  搶占式多任務(wù)處理的主要缺點(diǎn)在于,必須使用一種同步機(jī)制來(lái)保護(hù)資源以避免它們被無(wú)序訪問(wèn)。除此之外,還有另一種多任務(wù)管理模型,被稱為協(xié)調(diào)式多任務(wù)管理,其中線程間的切換將由線程自己負(fù)責(zé)完成。該模型普遍認(rèn)為太過(guò)危險(xiǎn),原因在于線程間的切換不發(fā)生的風(fēng)險(xiǎn)太大。如我們?cè)?.2.8節(jié)中所解釋的那樣,該機(jī)制會(huì)在內(nèi)部使用以提升某些服務(wù)器的性能,例如SQL Server2005。但Windows操作系統(tǒng)僅僅實(shí)現(xiàn)了搶占式多任務(wù)處理。

  5.3.4 進(jìn)程與線程的優(yōu)先級(jí)

  某些任務(wù)擁有比其他任務(wù)更高的優(yōu)先級(jí),它們需要操作系統(tǒng)為它們申請(qǐng)更多的處理時(shí)間。例如,某些由主處理器負(fù)責(zé)的外圍驅(qū)動(dòng)器必須不能被中斷。另一類高優(yōu)先級(jí)的任務(wù)就是圖形用戶界面。事實(shí)上,用戶不喜歡等待用戶界面被重繪。

  那些從Win32世界來(lái)的用戶知道在CLR的底層,也就是Windows操作系統(tǒng)中,可以為每個(gè)線程賦予一個(gè)0~31的優(yōu)先級(jí)。但你無(wú)法在.NET的世界中也使用這些數(shù)值,因?yàn)椋?/p>

  它們無(wú)法描述自身的含義。

  隨著時(shí)間的流逝這些值是非常容易變化的。

  1. 進(jìn)程的優(yōu)先級(jí)

  可以使用Process類中的類型為ProcessPriorityClass的PriorityClass{get;set;}屬性為進(jìn)程賦予一個(gè)優(yōu)先級(jí)。System.Diagnostics.ProcessPriorityClass枚舉包含以下值:

  如果某個(gè)進(jìn)程中屬于Process類的PriorityBoostEnabled屬性的值為true(默認(rèn)值為true),那么當(dāng)該進(jìn)程占據(jù)前臺(tái)窗口的時(shí)候,它的優(yōu)先級(jí)將增加一個(gè)單位。只有當(dāng)Process類的實(shí)例引用的是本機(jī)進(jìn)程時(shí),才能夠訪問(wèn)該屬性。

  可以通過(guò)以下操作利用任務(wù)管理器來(lái)改變一個(gè)進(jìn)程的優(yōu)先級(jí):在所選的進(jìn)程上點(diǎn)擊右鍵>設(shè)置優(yōu)先級(jí)>從提供的6個(gè)值(和上圖所述一致)中做出選擇。

  Windows操作系統(tǒng)有一個(gè)優(yōu)先級(jí)為0的空閑進(jìn)程。該進(jìn)程不能被其他任何進(jìn)程使用。根據(jù)定義,進(jìn)程的活躍度用時(shí)間的百分比表示為:100%減去在空閑進(jìn)程中所耗費(fèi)時(shí)間的比率。

  2. 線程的優(yōu)先級(jí)

  每個(gè)線程可以結(jié)合它所屬進(jìn)程的優(yōu)先級(jí),并使用System.Threading.Thread類中類型為T(mén)hreadPriority的Priority{get;set;}屬性定義各自的優(yōu)先級(jí)。System.Threading.Thread- Priority包含以下枚舉值:

  在大多數(shù)應(yīng)用程序中,不需要修改進(jìn)程和線程的優(yōu)先級(jí),它們的默認(rèn)值為Normal。

  5.3.5 System.Threading.Thread類

  CLR會(huì)自動(dòng)將一個(gè)System.Threading.Thread類的實(shí)例與各個(gè)受托管的線程關(guān)聯(lián)起來(lái)?梢允褂迷搶(duì)象從線程自身或從其他線程來(lái)操縱線程。還可以通過(guò)System.Threading.Thread類的靜態(tài)屬性CurrentThread來(lái)獲得當(dāng)前線程的對(duì)象。

  Thread類有一個(gè)功能使我們能夠很方便的調(diào)試多線程應(yīng)用程序,該功能允許我們使用一個(gè)字符串為線程命名:

  5.3.6 創(chuàng)建與Join一個(gè)線程

  只需通過(guò)創(chuàng)建一個(gè)Thread類的實(shí)例,就可以在當(dāng)前的進(jìn)程中創(chuàng)建一個(gè)新的線程。該類擁有多個(gè)構(gòu)造函數(shù),它們將接受一個(gè)類型為System.Threading.ThreadStart或System.Threading.Parame-trizedThreadStart的委托對(duì)象作為參數(shù),線程被創(chuàng)建出來(lái)后首先執(zhí)行該委托對(duì)象所引用的方法。使用ParametrizedThreadStart類型的委托對(duì)象允許用戶為新線程將要執(zhí)行的方法傳入一個(gè)對(duì)象作為參數(shù)。Thread類的一些構(gòu)造函數(shù)還接受一個(gè)整型參數(shù)用于設(shè)置線程要使用的最大棧的大小,該值至少為128KB(即131072字節(jié))。創(chuàng)建了Thread類型的實(shí)例后,必須調(diào)用Thread.Start()方法以真正啟動(dòng)這個(gè)線程。

  例5-3

  該程序輸出:

  在這個(gè)例子中,我們使用Join()方法掛起當(dāng)前線程,直到調(diào)用Join()方法的線程執(zhí)行完畢。該方法還存在包含參數(shù)的重載版本,其中的參數(shù)用于指定等待線程結(jié)束的最長(zhǎng)時(shí)間(即超時(shí))所花費(fèi)的毫秒數(shù)。如果線程中的工作在規(guī)定的超時(shí)時(shí)段內(nèi)結(jié)束,該版本的Join()方法將返回一個(gè)布爾量True。

  5.3.7 掛起一個(gè)線程

  可以使用Thread類的Sleep()方法將一個(gè)正在執(zhí)行的線程掛起一段特定的時(shí)間,還可以通過(guò)一個(gè)以毫秒為單位的整型值或者一個(gè)System.TimeSpan結(jié)構(gòu)的實(shí)例設(shè)定這段掛起的時(shí)間。該結(jié)構(gòu)的一個(gè)實(shí)例可以設(shè)定一個(gè)精度為1/10 ms(100ns)的時(shí)間段,但是Sleep()方法的最高精度只有1ms。

  我們也可以從將要掛起的線程自身或者另一個(gè)線程中使用Thread類的Suspend()方法將一個(gè)線程的活動(dòng)掛起。在這兩種情況中,線程都將被阻塞直到另一個(gè)線程調(diào)用了Resume()方法。相對(duì)于Sleep()方法,Suspend()方法不會(huì)立即將線程掛起,而是在線程到達(dá)下一個(gè)安全點(diǎn)之后,CLR才會(huì)將該線程掛起。安全點(diǎn)的概念參見(jiàn)4.7.11節(jié)。

  5.3.8 終止一個(gè)線程

  一個(gè)線程可以在以下場(chǎng)景中將自己終止。

  從自己開(kāi)始執(zhí)行的方法(主線程中的Main()方法,其他線程中ThreadStart委托對(duì)象所引用的方法)中退出。

  被自己終止。

  被另一個(gè)線程終止。

  第一種情況不太重要,我們將主要關(guān)注另兩種情況。在這兩種情況中,都可以使用Abort()方法(通過(guò)當(dāng)前線程或從當(dāng)前線程之外的一個(gè)線程)。使用該方法將在線程中引發(fā)一個(gè)類型為T(mén)hreadAbortException的異常。由于線程正處于一種被稱為AbortRequested的特殊狀態(tài),該異常具有一個(gè)特殊之處:當(dāng)它被異常處理所捕獲后,將自動(dòng)被重新拋出。只有在異常處理中調(diào)用Thread.ResetAbort()這個(gè)靜態(tài)方法(如果我們有足夠的權(quán)限)才能阻止它的傳播。

  例5-4 主線程的自殺

  當(dāng)線程A對(duì)線程B調(diào)用了Abort()方法,建議調(diào)用B的Join()方法,讓A一直等待直到B終止。Interrupt()方法也可以將一個(gè)處于阻塞狀態(tài)的線程(即由于調(diào)用了Wait()、Sleep()或者Join()其中一個(gè)方法而阻塞)終止。該方法會(huì)根據(jù)要被終止的線程是否處于阻塞狀態(tài)而表現(xiàn)出不同的行為。

  如果該方法被另一個(gè)線程調(diào)用時(shí),要被終止的線程處于阻塞狀態(tài),那么會(huì)產(chǎn)生ThreadInterruptedException異常。

  如果該方法被另一個(gè)線程調(diào)用時(shí),要被終止的線程不處于阻塞狀態(tài),那么一旦該線程進(jìn)入阻塞狀態(tài),就會(huì)引發(fā)異常。這種行為與線程對(duì)自己調(diào)用Interrupt()方法是一樣的。

  5.3.9 前臺(tái)線程與后臺(tái)線程

  Thread類提供了IsBackground{get;set}的布爾屬性。當(dāng)前臺(tái)線程還在運(yùn)行時(shí),它會(huì)阻止進(jìn)程被終止。另一方面,一旦所指的進(jìn)程中不再有前臺(tái)線程,后臺(tái)線程就會(huì)被CLR自動(dòng)終止(調(diào)用Abort()方法)。IsBackground的默認(rèn)值為false,這意味著所有的線程默認(rèn)情況處于前臺(tái)狀態(tài)。

  5.3.10 受托管線程

  Thread類擁有一個(gè)System.Threading.ThreadState枚舉類型的字段ThreadState,它包含以下枚舉值:

  有關(guān)每個(gè)狀態(tài)的具體描述可以在MSDN上一篇名為“ThreadStateEnumeration”的文章中找到。該枚舉類型是一個(gè)二進(jìn)制位域,這表示一個(gè)該類型的實(shí)例可以同時(shí)表示多個(gè)枚舉值。例如,一個(gè)線程可以同時(shí)處于Running、AbortRequested和Background這三種狀態(tài)。二進(jìn)制位域的概念參見(jiàn)10.11.3節(jié)。

  根據(jù)我們?cè)谇懊娴恼鹿?jié)中所了解的知識(shí),我們定義了如圖5-1所示的簡(jiǎn)化的狀態(tài)圖。

  圖5-1 簡(jiǎn)化的托管線程

  5.4 訪問(wèn)資源同步簡(jiǎn)介

  在多線程應(yīng)用(一個(gè)或多個(gè)處理器)的計(jì)算中會(huì)使用到同步這個(gè)詞。實(shí)際上,這些應(yīng)用程序的特點(diǎn)就是它們擁有多個(gè)執(zhí)行單元,而這些單元在訪問(wèn)資源的時(shí)候可能會(huì)發(fā)生沖突。線程間會(huì)共享同步對(duì)象,而同步對(duì)象的目的在于能夠阻塞一個(gè)或多個(gè)線程,直到另一個(gè)線程使得某個(gè)特定條件得到滿足。

  我們將看到,存在多種同步類與同步機(jī)制,每種制針對(duì)一個(gè)或一些特定的需求。如果要利用同步構(gòu)建一個(gè)復(fù)雜的多線程應(yīng)用程序,那么很有必要先掌握本章的內(nèi)容。我們將在下面的內(nèi)容中盡力區(qū)分他們,尤其要指出那些在各個(gè)機(jī)制間最微妙的區(qū)別。

  合理地同步一個(gè)程序是最精細(xì)的軟件開(kāi)發(fā)任務(wù)之一,單這一個(gè)主題就足以寫(xiě)幾本書(shū)。在深入到細(xì)節(jié)之前,應(yīng)該首先確認(rèn)使用同步是否不可避免。通常,使用一些簡(jiǎn)單的規(guī)則可以讓我們遠(yuǎn)離同步問(wèn)題。在這些規(guī)則中有線程與資源的親緣性規(guī)則,我們將在稍后介紹。

  應(yīng)該意識(shí)到,對(duì)程序中資源的訪問(wèn)進(jìn)行同步時(shí),其難點(diǎn)來(lái)自于是使用細(xì)粒度鎖還是粗粒度鎖這個(gè)兩難的選擇。如果在訪問(wèn)資源時(shí)采用粗粒度的同步方式,雖然可以簡(jiǎn)化代碼但是也會(huì)把自己暴露在爭(zhēng)用瓶頸的問(wèn)題上。如果粒度過(guò)細(xì),代碼又會(huì)變的很復(fù)雜,以至于維護(hù)工作令人生厭。然后又會(huì)遇上死鎖和競(jìng)態(tài)條件這些在下面章節(jié)將要介紹的問(wèn)題。

  因此在我們開(kāi)始談?wù)撚嘘P(guān)同步機(jī)制之前,有必要先了解一下有關(guān)競(jìng)態(tài)條件和死鎖的概念。

  5.4.1 競(jìng)態(tài)條件

  競(jìng)態(tài)條件指的是一種特殊的情況,在這種情況下各個(gè)執(zhí)行單元以一種沒(méi)有邏輯的順序執(zhí)行動(dòng)作,從而導(dǎo)致意想不到的結(jié)果。

  舉一個(gè)例子,線程T修改資源R后,釋放了它對(duì)R的寫(xiě)訪問(wèn)權(quán),之后又重新奪回R的讀訪問(wèn)權(quán)再使用它,并以為它的狀態(tài)仍然保持在它釋放它之后的狀態(tài)。但是在寫(xiě)訪問(wèn)權(quán)釋放后到重新奪回讀訪問(wèn)權(quán)的這段時(shí)間間隔中,可能另一個(gè)線程已經(jīng)修改了R的狀態(tài)。

  另一個(gè)經(jīng)典的競(jìng)態(tài)條件的例子就是生產(chǎn)者/消費(fèi)者模型。生產(chǎn)者通常使用同一個(gè)物理內(nèi)存空間保存被生產(chǎn)的信息。一般說(shuō)來(lái),我們不會(huì)忘記在生產(chǎn)者與消費(fèi)者的并發(fā)訪問(wèn)之間保護(hù)這個(gè)空間。容易被我們忘記的是生產(chǎn)者必須確保在生產(chǎn)新信息前,舊的信息已被消費(fèi)者所讀取。如果我們沒(méi)有采取相應(yīng)的預(yù)防措施,我們將面臨生產(chǎn)的信息從未被消費(fèi)的危險(xiǎn)。

  如果靜態(tài)條件沒(méi)有被妥善的管理,將導(dǎo)致安全系統(tǒng)的漏洞。同一個(gè)應(yīng)用程序的另一個(gè)實(shí)例很可能會(huì)引發(fā)一系列開(kāi)發(fā)者所預(yù)計(jì)不到的事件。一般來(lái)說(shuō),必須對(duì)那種用于確認(rèn)身份鑒別結(jié)果的布爾量的寫(xiě)訪問(wèn)做最完善的保護(hù)。如果沒(méi)有這么做,那么在它的狀態(tài)被身份鑒別機(jī)制設(shè)置后,到它被讀取以保護(hù)對(duì)資源的訪問(wèn)的這段時(shí)間內(nèi),很有可能已經(jīng)被修改了。已知的安全漏洞很多都?xì)w咎于對(duì)靜態(tài)條件不恰當(dāng)?shù)墓芾。其中之一甚至影響了Unix操作系統(tǒng)的內(nèi)核。

  5.4.2 死鎖

  死鎖指的是由于兩個(gè)或多個(gè)執(zhí)行單元之間相互等待對(duì)方結(jié)束而引起阻塞的情況。例如:

  一個(gè)線程T1獲得了對(duì)資源R1的訪問(wèn)權(quán)。

  一個(gè)線程T2獲得了對(duì)資源R2的訪問(wèn)權(quán)。

  T1請(qǐng)求對(duì)R2的訪問(wèn)權(quán)但是由于此權(quán)力被T2所占而不得不等待。

  T2請(qǐng)求對(duì)R1的訪問(wèn)權(quán)但是由于此權(quán)力被T1所占而不得不等待。

  T1和T2將永遠(yuǎn)維持等待狀態(tài),此時(shí)我們陷入了死鎖的處境!這種問(wèn)題比你所遇到的大多數(shù)的bug都要隱秘,針對(duì)此問(wèn)題主要有三種解決方案:

  在同一時(shí)刻不允許一個(gè)線程訪問(wèn)多個(gè)資源。

  為資源訪問(wèn)權(quán)的獲取定義一個(gè)關(guān)系順序。換句話說(shuō),當(dāng)一個(gè)線程已經(jīng)獲得了R1的訪問(wèn)權(quán)后,將無(wú)法獲得R2的訪問(wèn)權(quán)。當(dāng)然,訪問(wèn)權(quán)的釋放必須遵循相反的順序。

  為所有訪問(wèn)資源的請(qǐng)求系統(tǒng)地定義一個(gè)最大等待時(shí)間(超時(shí)時(shí)間),并妥善處理請(qǐng)求失敗的情況。幾乎所有的.NET的同步機(jī)制都提供了這個(gè)功能。

  前兩種技術(shù)效率更高但是也更加難于實(shí)現(xiàn)。事實(shí)上,它們都需要很強(qiáng)的約束,而這點(diǎn)隨著應(yīng)用程序的演變將越來(lái)越難以維護(hù)。盡管如此,使用這些技術(shù)不會(huì)存在失敗的情況。

  大的項(xiàng)目通常使用第三種方法。事實(shí)上,如果項(xiàng)目很大,一般來(lái)說(shuō)它會(huì)使用大量的資源。在這種情況下,資源之間發(fā)生沖突的概率很低,也就意味著失敗的情況會(huì)比較罕見(jiàn)。我們認(rèn)為這是一種樂(lè)觀的方法。秉著同樣的精神,我們?cè)?9.5節(jié)描述了一種樂(lè)觀的數(shù)據(jù)庫(kù)訪問(wèn)模型。

  5.5 使用volatile字段與Interlocked類實(shí)現(xiàn)同步

  5.5.1 volatile字段

  volatile字段可以被多個(gè)線程訪問(wèn)。我們假設(shè)這些訪問(wèn)沒(méi)有做任何同步。在這種情況下,CLR中一些用于管理代碼和內(nèi)存的內(nèi)部機(jī)制將負(fù)責(zé)同步工作,但是此時(shí)不能確保對(duì)該字段讀訪問(wèn)總能讀取到最新的值,而聲明為volatile的字段則能提供這樣的保證。在C#中,如果一個(gè)字段在它的聲明前使用了volatile關(guān)鍵字,則該字段被聲明為volatile。

  不是所有的字段都可以成為volatile,成為這種類型的字段有一個(gè)條件。如果一個(gè)字段要成為volatile,它的類型必須是以下所列的類型中的一種:

  引用類型(這里只有訪問(wèn)該類型的引用是同步的,訪問(wèn)其成員并不同步)。

  一個(gè)指針(在不安全的代碼塊中)。

  sbyte、byte、short、ushort、int、uint、char、float、bool(工作在64位處理器上時(shí)為double、long與ulong)。

  一個(gè)使用以下底層類型的枚舉類型:byte、sbyte、short、ushort、int、uint(工作在64位的處理器上時(shí)為double、long與ulong)。

  你可能已經(jīng)注意到了,只有值或者引用的位數(shù)不超過(guò)本機(jī)整型值的位數(shù)(4或8由底層處理器決定)的類型才能成為volatile。這意味著對(duì)更大的值類型進(jìn)行并發(fā)訪問(wèn)必須進(jìn)行同步,下面我們將會(huì)對(duì)此進(jìn)行討論。

  5.5.2 System.Threading.Interlocked類

  經(jīng)驗(yàn)顯示,那些需要在多線程情況下被保護(hù)的資源通常是整型值,而這些被共享的整型值最常見(jiàn)的操作就是增加/減少以及相加。.NETFramework利用System.Threading.Interlocked類提供了一個(gè)專門(mén)的機(jī)制用于完成這些特定的操作。這個(gè)類提供了Increment()、Decrement()與Add()三個(gè)靜態(tài)方法,分別用于對(duì)int或者long類型變量的遞增、遞減與相加操作,這些變量以引用方式作為參數(shù)傳入。我們認(rèn)為使用Interlocked類讓這些操作具有了原子性。

  下面的程序顯示了兩個(gè)線程如何并發(fā)訪問(wèn)一個(gè)名為counter的整型變量。一個(gè)線程將其遞增5次,另一個(gè)將其遞減5次。

  例5-5

  該程序輸出(以非確定方式輸出,意味著每執(zhí)行一次顯示的結(jié)果都是不同的):

  如果我們不讓這些線程在每次修改變量后休眠10毫秒,那么它們將有足夠的時(shí)間在一個(gè)時(shí)間片中完成它們的任務(wù),那樣也就不會(huì)出現(xiàn)交叉操作,更不用說(shuō)并發(fā)訪問(wèn)了。

  5.5.3 Interlocked類提供的其他功能

  Interlocked類還允許使用Exchange()靜態(tài)方法,以原子操作的形式交換某些變量的狀態(tài)。還可以使用CompareExchange()靜態(tài)方法在滿足一個(gè)特定條件的基礎(chǔ)上以原子操作的形式交換兩個(gè)值。

  5.6 使用System.Threading.Monitor類與C#的lock關(guān)鍵字實(shí)現(xiàn)同步

  以原子操作的方式完成簡(jiǎn)單的操作無(wú)疑是很重要的,但是這還遠(yuǎn)不能涵蓋所有需要用到同步的事例。System.Threading.Monitor類幾乎允許將任意一段代碼設(shè)置為在某個(gè)時(shí)間僅能被一個(gè)線程執(zhí)行。我們將這段代碼稱之為臨界區(qū)。

  5.6.1 Enter()方法和Exit()方法

  Monitor類提供了Enter(object)與Exit(object)這兩個(gè)靜態(tài)方法。這兩個(gè)方法以一個(gè)對(duì)象作為參數(shù),該對(duì)象提供了一個(gè)簡(jiǎn)單的方式用于唯一標(biāo)識(shí)那個(gè)將以同步方式訪問(wèn)的資源。當(dāng)一個(gè)線程調(diào)用了Enter()方法,它將等待以獲得訪問(wèn)該引用對(duì)象的獨(dú)占權(quán)(僅當(dāng)另一個(gè)線程擁有該權(quán)力的時(shí)候它才會(huì)等待)。一旦該權(quán)力被獲得并使用,線程可以對(duì)同一個(gè)對(duì)象調(diào)用Exit()方法以釋放該權(quán)力。

  一個(gè)線程可以對(duì)同一個(gè)對(duì)象多次調(diào)用Enter(),只要對(duì)同一對(duì)象調(diào)用相同次數(shù)的Exit()來(lái)釋放獨(dú)占訪問(wèn)權(quán)。

  一個(gè)線程也可以在同一時(shí)間擁有多個(gè)對(duì)象的獨(dú)占權(quán),但是這樣會(huì)產(chǎn)生死鎖的情況。

  絕不能對(duì)一個(gè)值類型的實(shí)例調(diào)用Enter()與Exit()方法。

  不管發(fā)生了什么,必須在finally子句中調(diào)用Exit()以釋放所有的獨(dú)占訪問(wèn)權(quán)。

  如果在例5-5中,一個(gè)線程非要將counter做一次平方而另一個(gè)線程非要將counter乘2,我們就不得不用Monitor類去替換對(duì)Interlocked類的使用。f1()與f2()的代碼將變成下面這樣:

  例5-6[1]

  人們很容易想到用counter來(lái)代替typeof(Program),但是counter是一個(gè)值類型的靜態(tài)成員。需要注意平方和倍增操作是不滿足交換律的,所以counter的最終結(jié)果是非確定性的。

  5.6.2 C#的lock關(guān)鍵字

  C#語(yǔ)言通過(guò)lock關(guān)鍵字提供了一種比使用Enter()和Exit()方法更加簡(jiǎn)潔的選擇。我們的程序可以改寫(xiě)為下面這個(gè)樣子:

  例5-7

  和for以及if關(guān)鍵字一樣,如果被lock關(guān)鍵字定義的塊僅包含一條指令,就不再需要花括號(hào)。我們可以再次改寫(xiě)為:

  使用lock關(guān)鍵字將引導(dǎo)C#編譯器創(chuàng)建出相應(yīng)的try/finally塊,這樣仍舊可以預(yù)期到任何可能引發(fā)的異常。可以使用Reflector或者ildasm.exe工具驗(yàn)證這一點(diǎn)。

  5.6.3 SyncRoot模式

  和前面的例子一樣,我們通常在一個(gè)靜態(tài)方法中使用Monitor類配合一個(gè)Type類的實(shí)例。同樣,我們往往會(huì)在一個(gè)非靜態(tài)方法中使用this關(guān)鍵字來(lái)實(shí)現(xiàn)同步。在兩種情況下,我們都是通過(guò)一個(gè)在類外部可見(jiàn)的對(duì)象對(duì)自身進(jìn)行同步。如果其他部分的代碼也利用這些對(duì)象來(lái)實(shí)現(xiàn)自身的同步,就會(huì)出現(xiàn)問(wèn)題。為了避免這種潛在的問(wèn)題,我們推薦使用一個(gè)類型為object的名為SyncRoot的私有成員,至于該成員是靜態(tài)的還是非靜態(tài)的則由需要而定。

  例5-8

  System.Collections.ICollection接口提供了object類型的SyncRoot{get;}屬性。大多數(shù)的集合類(泛型或非泛型)都實(shí)現(xiàn)了該接口。同樣地,可以使用該屬性同步對(duì)集合中元素的訪問(wèn)。不過(guò)在這里SyncRoot模式并沒(méi)有被真正的應(yīng)用,因?yàn)槲覀儗?duì)訪問(wèn)進(jìn)行同步所使用對(duì)象不是私有的。

  例5-9

  5.6.4 線程安全類

  若一個(gè)類的每個(gè)實(shí)例在同一時(shí)間不能被一個(gè)以上的線程所訪問(wèn),則該類稱之為一個(gè)線程安全的類。為了創(chuàng)建一個(gè)線程安全的類,只需將我們見(jiàn)過(guò)的SyncRoot模式應(yīng)用于它所包含的方法。如果一個(gè)類想變成線程安全的,而又不想為類中代碼增加過(guò)多負(fù)擔(dān),那么有一個(gè)好方法就是像下面這樣為其提供一個(gè)經(jīng)過(guò)線程安全包裝的繼承類。

  例5-10

  另一種方法就是使用System.Runtime.Remoting.Contexts.SynchronizationAttribute,這點(diǎn)我們將在本章稍后討論。

  5.6.5 Monitor.TryEnter()方法

  該方法與Enter()相似,只不過(guò)它是非阻塞的。如果資源的獨(dú)占訪問(wèn)權(quán)已經(jīng)被另一個(gè)線程占據(jù),該方法將立即返回一個(gè)false返回值。我們也可以調(diào)用TryEnter()方法,讓它以毫秒為單位阻塞一段有限的時(shí)間。因?yàn)樵摲椒ǖ姆祷亟Y(jié)果并不確定,并且當(dāng)獲得獨(dú)占訪問(wèn)權(quán)后必須在finally子句中釋放該權(quán)力,所以建議當(dāng)TryEnter()失敗時(shí)立即退出正在調(diào)用的函數(shù):

  例5-11[2]

  5.6.6 Monitor類的Wait()方法, Pulse()方法以及PulseAll()方法

  Wait()、Pulse()與PulseAll()方法必須在一起使用并且需要結(jié)合一個(gè)小場(chǎng)景才能被正確理解。我們的想法是這樣的:一個(gè)線程獲得了某個(gè)對(duì)象的獨(dú)占訪問(wèn)權(quán),而它決定等待(通過(guò)調(diào)用Wait())直到該對(duì)象的狀態(tài)發(fā)生變化。為此,該線程必須暫時(shí)失去對(duì)象獨(dú)占訪問(wèn)權(quán),以便讓另一個(gè)線程修改對(duì)象的狀態(tài)。修改對(duì)象狀態(tài)的線程必須使用Pulse()方法通知那個(gè)等待線程修改完成。下面有一個(gè)小場(chǎng)景具體說(shuō)明了這一情況。

  擁有OBJ對(duì)象獨(dú)占訪問(wèn)權(quán)的T1線程,調(diào)用Wait(OBJ)方法將它自己注冊(cè)到OBJ對(duì)象的被動(dòng)等待列表中。

  由于以上的調(diào)用,T1失去了對(duì)OBJ的獨(dú)占訪問(wèn)權(quán)。因此,另一個(gè)線程T2通過(guò)調(diào)用Enter(OBJ)獲得OBJ的獨(dú)占訪問(wèn)權(quán)。

  T2最終修改了OBJ的狀態(tài)并調(diào)用Pulse(OBJ)通知了這次修改。該調(diào)用將導(dǎo)致OBJ被動(dòng)等待列表中的第一個(gè)線程(在這里是T1)被移到OBJ的主動(dòng)等待列表的首位。而一旦OBJ的獨(dú)占訪問(wèn)權(quán)被釋放,OBJ主動(dòng)等待列表中的第一個(gè)線程將被確保獲得該權(quán)力。然后它就從Wait(OBJ)方法中退出等待狀態(tài)。

  在我們的場(chǎng)景中,T2調(diào)用Exit(OBJ)以釋放對(duì)OBJ的獨(dú)占訪問(wèn)權(quán),接著T1恢復(fù)訪問(wèn)權(quán)并從Wait(OBJ)方法中退出。

  PulseAll()將使得被動(dòng)等待列表中的線程全部轉(zhuǎn)移到主動(dòng)等待列表中。注意這些線程將按照它們調(diào)用Wait()的順序到達(dá)非阻塞態(tài)。

  如果Wait(OBJ)被一個(gè)調(diào)用了多次Enter(OBJ)的線程所調(diào)用,那么該線程將需要調(diào)用相同次數(shù)的Exit(OBJ)以釋放對(duì)OBJ的訪問(wèn)權(quán)。即使在這種情況下,另一個(gè)線程調(diào)用一次Pulse(OBJ)就足以將第一個(gè)線程變成非阻塞態(tài)。

  下面的程序通過(guò)ping與pong兩個(gè)線程以交替的方式使用一個(gè)ball對(duì)象的訪問(wèn)權(quán)來(lái)演示該功能。

  例5-12

  該程序輸出(以不確定的方式):

  pong線程沒(méi)有結(jié)束并且仍然阻塞在Wait()方法上。由于pong線程是第二個(gè)獲得ball對(duì)象的獨(dú)占訪問(wèn)權(quán)的,所以才導(dǎo)致了該結(jié)果。

【進(jìn)程和線程的區(qū)別】相關(guān)文章:

設(shè)計(jì)并發(fā)服務(wù)器,使用多進(jìn)程與多線程有什么區(qū)別?12-03

關(guān)于linux查看進(jìn)程ps top區(qū)別09-30

Linux多線程:線程的分離與結(jié)合08-17

ssat和托福的區(qū)別10-14

咖啡的種類和區(qū)別10-09

速錄和速記的區(qū)別10-01

綠茶和白茶的區(qū)別10-22

打工和創(chuàng)業(yè)的區(qū)別11-02

紋眉和畫(huà)眉的區(qū)別07-19

朗誦和播音的區(qū)別09-04