亚洲AV日韩AⅤ综合手机在线观看,激情婷婷久久综合色,欧美色五月婷婷久久,久久国产精品99久久人人澡

  • <abbr id="uk6uq"><abbr id="uk6uq"></abbr></abbr>
  • <tbody id="uk6uq"></tbody>
  • 進(jìn)程和線程的區(qū)別

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

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

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

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

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

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

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

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

      進(jìn)程是具有一定獨(dú)立功能的程序關(guān)于某個(gè)數(shù)據(jù)集合上的一次運(yùn)行活動,進(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ù)模式下不會對其它進(jìn)程產(chǎn)生影響,而線程只是一個(gè)進(jìn)程中的不同執(zhí)行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨(dú)的地址空間,一個(gè)線程死掉就等于整個(gè)進(jìn)程死掉,所以多進(jìn)程的程序要比多線程的程序健壯,但在進(jìn)程切換時(shí),耗費(fèi)資源較大,效率要差一些。但對于一些要求同時(shí)進(jìn)行并且又要共享某些變量的并發(fā)操作,只能用線程,不能用進(jìn)程。如果有興趣深入的話,我建議你們看看《現(xiàn)代操作系統(tǒng)》或者《操作系統(tǒng)的設(shè)計(jì)與實(shí)現(xiàn)》。對就個(gè)問題說得比較清楚。

      5.1 簡介

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

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

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

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

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

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

      5.2 進(jìn)程

      5.2.1 簡介

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

      如果有N個(gè)進(jìn)程運(yùn)行在同一臺機(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)存頁,并根據(jù)使用情況將這些內(nèi)存頁存儲在硬盤上或加載到RAM中,通過系統(tǒng)的這種虛擬內(nèi)存機(jī)制,我們可以有效地減少對實(shí)際內(nèi)存的需求量。當(dāng)然這些對用戶和開發(fā)者來說都是透明的。

      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)程。

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

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

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

      例5-1

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

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

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

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

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

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

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

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

      例5-2

      通過方法參數(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)前對象的所有清理工作以及finally塊的執(zhí)行都將由不同的線程完成。當(dāng)然,終止進(jìn)程將花費(fèi)一定的時(shí)間。

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

      5.3 線程

      5.3.1 簡介

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

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

      一個(gè)棧;

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

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

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

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

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

      同樣,幾年前我們就注意到,預(yù)測每18個(gè)月處理器運(yùn)算速度增加一倍的摩爾定律已不再成立。處理器的頻率停滯在3GHz~4GHz上下。這是由于物理上的限制,需要一段時(shí)間才能取得突破。同時(shí),為了在性能競爭中不會落敗,較大的處理器制造商如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í),我們將它稱為受托管的線程。具體來說,由受托管的代碼創(chuàng)建出來的線程就是受托管的線程。如果一個(gè)線程由非托管的代碼所創(chuàng)建,那么它就是非托管的線程。不過,一旦該線程執(zhí)行了受托管的代碼它就變成了受托管的線程。

      一個(gè)受托管的線程和非托管的線程的區(qū)別在于,CLR將創(chuàng)建一個(gè)System.Threading.Thread類的實(shí)例來代表并操作前者。在內(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的概念參見4.1。

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

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

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

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

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

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

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

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

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

      它們無法描述自身的含義。

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

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

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

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

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

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

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

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

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

      5.3.5 System.Threading.Thread類

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

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

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

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

      例5-3

      該程序輸出:

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

      5.3.7 掛起一個(gè)線程

      可以使用Thread類的Sleep()方法將一個(gè)正在執(zhí)行的線程掛起一段特定的時(shí)間,還可以通過一個(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è)線程的活動掛起。在這兩種情況中,線程都將被阻塞直到另一個(gè)線程調(diào)用了Resume()方法。相對于Sleep()方法,Suspend()方法不會立即將線程掛起,而是在線程到達(dá)下一個(gè)安全點(diǎn)之后,CLR才會將該線程掛起。安全點(diǎn)的概念參見4.7.11節(jié)。

      5.3.8 終止一個(gè)線程

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

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

      被自己終止。

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

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

      例5-4 主線程的自殺

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

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

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

      5.3.9 前臺線程與后臺線程

      Thread類提供了IsBackground{get;set}的布爾屬性。當(dāng)前臺線程還在運(yùn)行時(shí),它會阻止進(jìn)程被終止。另一方面,一旦所指的進(jìn)程中不再有前臺線程,后臺線程就會被CLR自動終止(調(diào)用Abort()方法)。IsBackground的默認(rèn)值為false,這意味著所有的線程默認(rèn)情況處于前臺狀態(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)制位域的概念參見10.11.3節(jié)。

      根據(jù)我們在前面的章節(jié)中所了解的知識,我們定義了如圖5-1所示的簡化的狀態(tài)圖。

      圖5-1 簡化的托管線程

      5.4 訪問資源同步簡介

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

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

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

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

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

      5.4.1 競態(tài)條件

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

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

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

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

      5.4.2 死鎖

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

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

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

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

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

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

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

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

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

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

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

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

      5.5.1 volatile字段

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

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

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

      一個(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ù)不超過本機(jī)整型值的位數(shù)(4或8由底層處理器決定)的類型才能成為volatile。這意味著對更大的值類型進(jìn)行并發(fā)訪問必須進(jìn)行同步,下面我們將會對此進(jìn)行討論。

      5.5.2 System.Threading.Interlocked類

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

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

      例5-5

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

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

      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)同步

      以原子操作的方式完成簡單的操作無疑是很重要的,但是這還遠(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è)對象作為參數(shù),該對象提供了一個(gè)簡單的方式用于唯一標(biāo)識那個(gè)將以同步方式訪問的資源。當(dāng)一個(gè)線程調(diào)用了Enter()方法,它將等待以獲得訪問該引用對象的獨(dú)占權(quán)(僅當(dāng)另一個(gè)線程擁有該權(quán)力的時(shí)候它才會等待)。一旦該權(quán)力被獲得并使用,線程可以對同一個(gè)對象調(diào)用Exit()方法以釋放該權(quán)力。

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

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

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

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

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

      例5-6[1]

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

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

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

      例5-7

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

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

      5.6.3 SyncRoot模式

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

      例5-8

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

      例5-9

      5.6.4 線程安全類

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

      例5-10

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

      5.6.5 Monitor.TryEnter()方法

      該方法與Enter()相似,只不過它是非阻塞的。如果資源的獨(dú)占訪問權(quán)已經(jīng)被另一個(gè)線程占據(jù),該方法將立即返回一個(gè)false返回值。我們也可以調(diào)用TryEnter()方法,讓它以毫秒為單位阻塞一段有限的時(shí)間。因?yàn)樵摲椒ǖ姆祷亟Y(jié)果并不確定,并且當(dāng)獲得獨(dú)占訪問權(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è)小場景才能被正確理解。我們的想法是這樣的:一個(gè)線程獲得了某個(gè)對象的獨(dú)占訪問權(quán),而它決定等待(通過調(diào)用Wait())直到該對象的狀態(tài)發(fā)生變化。為此,該線程必須暫時(shí)失去對象獨(dú)占訪問權(quán),以便讓另一個(gè)線程修改對象的狀態(tài)。修改對象狀態(tài)的線程必須使用Pulse()方法通知那個(gè)等待線程修改完成。下面有一個(gè)小場景具體說明了這一情況。

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

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

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

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

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

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

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

      例5-12

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

      pong線程沒有結(jié)束并且仍然阻塞在Wait()方法上。由于pong線程是第二個(gè)獲得ball對象的獨(dú)占訪問權(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

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

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