[轉]進程和線程的區別

簡而言之,一個程序至少有一個進程,一個進程至少有一個線程. 
線程的劃分尺度小於進程,使得多線程程序的併發性高。
另外,進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率。
線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行。但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。html

進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位.
線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源.
一個線程能夠建立和撤銷另外一個線程;同一個進程中的多個線程之間能夠併發執行.web

進程和線程的主要差異在於它們是不一樣的操做系統資源管理方式。進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不一樣執行路徑。線程有本身的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,因此多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行而且又要共享某些變量的併發操做,只能用線程,不能用進程。若是有興趣深刻的話,我建議大家看看《現代操做系統》或者《操做系統的設計與實現》。對就個問題說得比較清楚。算法

5.1 簡介

進程(process)是一塊包含了某些資源的內存區域。操做系統利用進程把它的工做劃分爲一些功能單元。數據庫

進程中所包含的一個或多個執行單元稱爲線程(thread)。進程還擁有一個私有的虛擬地址空間,該空間僅能被它所包含的線程訪問。安全

當運行.NET程序時,進程還會把被稱爲CLR的軟件層包含到它的內存空間中。上一章曾經對CLR作了詳細描述。該軟件層是在進程建立期間由運行時宿主載入的(參見4.2.3節)。服務器

線程只能歸屬於一個進程而且它只能訪問該進程所擁有的資源。當操做系統建立一個進程後,該進程會自動申請一個名爲主線程或首要線程的線程。主線程將執行運行時宿主, 而運行時宿主會負責載入CLR。多線程

應用程序(application)是由一個或多個相互協做的進程組成的。例如,Visual Studio開發環境就是利用一個進程編輯源文件,並利用另外一個進程完成編譯工做的應用程序。架構

在Windows NT/2000/XP操做系統下,咱們能夠經過任務管理器在任意時間查看全部的應用程序和進程。儘管只打開了幾個應用程序,可是一般狀況下將有大約30個進程同時運行。 事實上,爲了管理當前的會話和任務欄以及其餘一些任務,系統執行了大量的進程。併發

5.2 進程

5.2.1 簡介

在運行於32位處理器上的32位Windows操做系統中,可將一個進程視爲一段大小爲4GB(232字節)的線性內存空間,它起始於0x00000000結束於0xFFFFFFFF。這段內存空間不能被其餘進程所訪問,因此稱爲該進程的私有空間。這段空間被平分爲兩塊,2GB被系統全部,剩下2GB被用戶全部。app

若是有N個進程運行在同一臺機器上,那麼將須要N×4GB的海量RAM,還好事實並不是如此。

  • Windows是按需爲每一個進程分配內存的,4GB是32位系統中一個進程所佔空間的上限。
  • 將進程所需的內存劃分爲4KB大小的內存頁,並根據使用狀況將這些內存頁存儲在硬盤上或加載到RAM中,經過系統的這種虛擬內存機制,咱們能夠有效地減小對實際內存的需求量。固然這些對用戶和開發者來講都是透明的。
5.2.2 System.Diagnostics.Process類

System.Diagnostics.Process類的實例能夠引用一個進程,被引用的進程包含如下幾種。

  • 該實例的當前進程。
  • 本機上除了當前進程的其餘進程。
  • 遠程機器上的某個進程。

經過該類所包含的方法和字段,能夠建立或銷燬一個進程,而且能夠得到一個進程的相關信息。下面將討論一些使用該類實現的常見任務。

5.2.3 建立和銷燬子進程

下面的程序建立了一個稱爲子進程的新進程。在這種狀況下,初始的進程稱爲父進程。子進程啓動了一個記事本應用程序。父進程的線程在等待1秒後銷燬該子進程。該程序的執行效果就是打開並關閉記事本。

例5-1

靜態方法Start()可使用已存在的Windows文件擴展名關聯機制。例如,咱們能夠利用下面的代碼執行一樣的操做。

默認狀況下,子進程將繼承其父進程的安全上下文。但還可使用Process.Start()方法的一個重載版本在任意用戶的安全上下文中啓動該子進程,固然須要經過一個System.Diagnostics. ProcessStartInfo類的實例來提供該用戶的用戶名和密碼。

5.2.4 避免在一臺機器上同時運行同一應用程序的多個實例

有些應用程序須要這種功能。實際上,一般來講在同一臺機器上同時運行一個應用程序的多個實例並無意義。

直到如今,爲了在Windows下知足上述約束,開發者最經常使用的方法仍然是使用有名互斥體(named mutex)技術(參見5.7.2節)。然而採用這種技術來知足上述約束存在如下缺點:

  • 該技術具備使互斥體的名字被其餘應用程序所使用的較小的、潛在的風險。在這種狀況下該技術將再也不有效而且會形成很難檢測到的bug。
  • 該技術不能解決咱們僅容許一個應用程序產生N個實例這種通常的問題。

幸而在System.Diagnostics.Process類中擁有GetCurrentProcess()(返回當前進程)和GetPro- cesses()(返回機器上全部的進程)這樣的靜態方法。在下面的程序中咱們爲上述問題找到了一個優雅且簡單的解決方案。

例5-2

經過方法參數指定了遠程機器的名字後,GetProcesses()方法也能夠返回遠程機器上全部的進程。

5.2.5 終止當前進程

能夠調用System.Environment類中的靜態方法Exit(int exitCode)或FailFast(stringmessage)終止當前進程。Exit()方法是最好的選擇,它將完全終止進程並向操做系統返回指定的退出代碼值。之因此稱爲完全終止是由於當前對象的全部清理工做以及finally塊的執行都將由不一樣的線程完成。固然,終止進程將花費必定的時間。

顧名思義,FailFast()方法能夠迅速終止進程。Exit()方法所作的預防措施將被它忽略。只有一個包含了指定信息的嚴重錯誤會被操做系統記錄到日誌中。你可能想要在探查問題的時候使用該方法,由於能夠將該程序的完全終止視爲數據惡化的原由。

5.3 線程

5.3.1 簡介

一個線程包含如下內容。

  • 一個指向當前被執行指令的指令指針;
  • 一個棧;
  • 一個寄存器值的集合,定義了一部分描述正在執行線程的處理器狀態的值;
  • 一個私有的數據區。

全部這些元素都歸於線程執行上下文的名下。處在同一個進程中的全部線程均可以訪問該進程所包含的地址空間,固然也包含存儲在該空間中的全部資源。

咱們不許備討論線程在內核模式或者用戶模式執行的問題。儘管.NET之前的Windows一直使用這兩種模式,而且依然存在,可是對.NET Framework來講它們是不可見的。

並行使用一些線程一般是咱們在實現算法時的天然反應。實際上,一個算法每每由一系列能夠併發執行的任務組成。可是須要引發注意的是,使用大量的線程將引發過多的上下文切換,最終反而影響了性能。

一樣,幾年前咱們就注意到,預測每18個月處理器運算速度增長一倍的摩爾定律已再也不成立。處理器的頻率停滯在3GHz~4GHz上下。這是因爲物理上的限制,須要一段時間才能取得突破。同時,爲了在性能競爭中不會落敗,較大的處理器製造商如AMD和Intel目前都將目標轉向多核芯片。所以咱們能夠預計在接下去的幾年中這種類型的架構將普遍被採用。在這種狀況下,改進應用性能的惟一方案就是合理地利用多線程技術。

5.3.2 受託管的線程與 Windows線程

必需要了解,執行.NET應用的線程實際上仍然是Windows線程。可是,當某個線程被CLR所知時,咱們將它稱爲受託管的線程。具體來講,由受託管的代碼建立出來的線程就是受託管的線程。若是一個線程由非託管的代碼所建立,那麼它就是非託管的線程。不過,一旦該線程執行了受託管的代碼它就變成了受託管的線程。

一個受託管的線程和非託管的線程的區別在於,CLR將建立一個System.Threading.Thread類的實例來表明並操做前者。在內部實現中,CLR將一個包含了全部受託管線程的列表保存在一個叫作ThreadStore地方。

CLR確保每個受託管的線程在任意時刻都在一個AppDomain中執行,可是這並不表明一個線程將永遠處在一個AppDomain中,它能夠隨着時間的推移轉到其餘的AppDomain中。關於AppDomain的概念參見4.1。

從安全的角度來看,一個受託管的線程的主用戶與底層的非託管線程中的Windows主用戶是無關的。

5.3.3 搶佔式多任務處理

咱們能夠問本身下面這個問題: 個人計算機只有一個處理器,然而在任務管理器中咱們卻能夠看到數以百計的線程正同時運行在機器上!這怎麼可能呢?

多虧了搶佔式多任務處理,經過它對線程的調度,使得上述問題成爲可能。調度器做爲Windows內核的一部分,將時間切片,分紅一段段的時間片。這些時間間隔以毫秒爲精度且長度並不固定。針對每一個處理器,每一個時間片僅服務於單獨一個線程。線程的迅速執行給咱們形成了它們在同時運行的假象。咱們在兩個時間片的間隔中進行上下文切換。該方法的優勢在於,那些正在等待某些Windows資源的線程將不會浪費時間片,直到資源有效爲止。

之因此用搶佔式這個形容詞來修飾這種多任務管理方式,是由於在此種方式下線程將被系統強制性中斷。那些對此比較好奇的人應該瞭解到,在上下文切換的過程當中,操做系統會在下一個線程將要執行的代碼中插入一條跳轉到下一個上下文切換的指令。該指令是一個軟中斷,若是線程在遇到這條指令前就終止了(例如,它正在等待某個資源),那麼該指定將被刪除而上下文切換也將提早發生。

搶佔式多任務處理的主要缺點在於,必須使用一種同步機制來保護資源以免它們被無序訪問。除此以外,還有另外一種多任務管理模型,被稱爲協調式多任務管理,其中線程間的切換將由線程本身負責完成。該模型廣泛認爲太過危險,緣由在於線程間的切換不發生的風險太大。如咱們在4.2.8節中所解釋的那樣,該機制會在內部使用以提高某些服務器的性能,例如SQL Server2005。但Windows操做系統僅僅實現了搶佔式多任務處理。

5.3.4 進程與線程的優先級

某些任務擁有比其餘任務更高的優先級,它們須要操做系統爲它們申請更多的處理時間。例如,某些由主處理器負責的外圍驅動器必須不能被中斷。另外一類高優先級的任務就是圖形用戶界面。事實上,用戶不喜歡等待用戶界面被重繪。

那些從Win32世界來的用戶知道在CLR的底層,也就是Windows操做系統中,能夠爲每一個線程賦予一個0~31的優先級。但你沒法在.NET的世界中也使用這些數值,由於:

  • 它們沒法描述自身的含義。
  • 隨着時間的流逝這些值是很是容易變化的。

1. 進程的優先級

可使用Process類中的類型爲ProcessPriorityClass的PriorityClass{get;set;}屬性爲進程賦予一個優先級。System.Diagnostics.ProcessPriorityClass枚舉包含如下值:

若是某個進程中屬於Process類的PriorityBoostEnabled屬性的值爲true(默認值爲true),那麼當該進程佔據前臺窗口的時候,它的優先級將增長一個單位。只有當Process類的實例引用的是本機進程時,纔可以訪問該屬性。

能夠經過如下操做利用任務管理器來改變一個進程的優先級:在所選的進程上點擊右鍵>設置優先級>從提供的6個值(和上圖所述一致)中作出選擇。

Windows操做系統有一個優先級爲0的空閒進程。該進程不能被其餘任何進程使用。根據定義,進程的活躍度用時間的百分比表示爲:100%減去在空閒進程中所耗費時間的比率。

2. 線程的優先級

每一個線程能夠結合它所屬進程的優先級,並使用System.Threading.Thread類中類型爲ThreadPriority的Priority{get;set;}屬性定義各自的優先級。System.Threading.Thread- Priority包含如下枚舉值:

在大多數應用程序中,不須要修改進程和線程的優先級,它們的默認值爲Normal。

5.3.5 System.Threading.Thread類

CLR會自動將一個System.Threading.Thread類的實例與各個受託管的線程關聯起來。可使用該對象從線程自身或從其餘線程來操縱線程。還能夠經過System.Threading.Thread類的靜態屬性CurrentThread來得到當前線程的對象。

Thread類有一個功能使咱們可以很方便的調試多線程應用程序,該功能容許咱們使用一個字符串爲線程命名:

5.3.6 建立與Join一個線程

只需經過建立一個Thread類的實例,就能夠在當前的進程中建立一個新的線程。該類擁有多個構造函數,它們將接受一個類型爲System.Threading.ThreadStart或System.Threading.Parame-trizedThreadStart的委託對象做爲參數,線程被建立出來後首先執行該委託對象所引用的方法。使用ParametrizedThreadStart類型的委託對象容許用戶爲新線程將要執行的方法傳入一個對象做爲參數。Thread類的一些構造函數還接受一個整型參數用於設置線程要使用的最大棧的大小,該值至少爲128KB(即131072字節)。建立了Thread類型的實例後,必須調用Thread.Start()方法以真正啓動這個線程。

例5-3

該程序輸出:

在這個例子中,咱們使用Join()方法掛起當前線程,直到調用Join()方法的線程執行完畢。該方法還存在包含參數的重載版本,其中的參數用於指定等待線程結束的最長時間(即超時)所花費的毫秒數。若是線程中的工做在規定的超時時段內結束,該版本的Join()方法將返回一個布爾量True。

5.3.7 掛起一個線程

可使用Thread類的Sleep()方法將一個正在執行的線程掛起一段特定的時間,還能夠經過一個以毫秒爲單位的整型值或者一個System.TimeSpan結構的實例設定這段掛起的時間。該結構的一個實例能夠設定一個精度爲1/10 ms(100ns)的時間段,可是Sleep()方法的最高精度只有1ms。

咱們也能夠從將要掛起的線程自身或者另外一個線程中使用Thread類的Suspend()方法將一個線程的活動掛起。在這兩種狀況中,線程都將被阻塞直到另外一個線程調用了Resume()方法。相對於Sleep()方法,Suspend()方法不會當即將線程掛起,而是在線程到達下一個安全點以後,CLR纔會將該線程掛起。安全點的概念參見4.7.11節。

5.3.8 終止一個線程

一個線程能夠在如下場景中將本身終止。

  • 從本身開始執行的方法(主線程中的Main()方法,其餘線程中ThreadStart委託對象所引用的方法)中退出。
  • 被本身終止。
  • 被另外一個線程終止。

第一種狀況不過重要,咱們將主要關注另兩種狀況。在這兩種狀況中,均可以使用Abort()方法(經過當前線程或從當前線程以外的一個線程)。使用該方法將在線程中引起一個類型爲ThreadAbortException的異常。因爲線程正處於一種被稱爲AbortRequested的特殊狀態,該異常具備一個特殊之處:當它被異常處理所捕獲後,將自動被從新拋出。只有在異常處理中調用Thread.ResetAbort()這個靜態方法(若是咱們有足夠的權限)才能阻止它的傳播。

例5-4 主線程的自殺

當線程A對線程B調用了Abort()方法,建議調用B的Join()方法,讓A一直等待直到B終止。Interrupt()方法也能夠將一個處於阻塞狀態的線程(即因爲調用了Wait()、Sleep()或者Join()其中一個方法而阻塞)終止。該方法會根據要被終止的線程是否處於阻塞狀態而表現出不一樣的行爲。

  • 若是該方法被另外一個線程調用時,要被終止的線程處於阻塞狀態,那麼會產生ThreadInterruptedException異常。
  • 若是該方法被另外一個線程調用時,要被終止的線程不處於阻塞狀態,那麼一旦該線程進入阻塞狀態,就會引起異常。這種行爲與線程對本身調用Interrupt()方法是同樣的。
5.3.9 前臺線程與後臺線程

Thread類提供了IsBackground{get;set}的布爾屬性。當前臺線程還在運行時,它會阻止進程被終止。另外一方面,一旦所指的進程中再也不有前臺線程,後臺線程就會被CLR自動終止(調用Abort()方法)。IsBackground的默認值爲false,這意味着全部的線程默認狀況處於前臺狀態。

5.3.10 受託管線程的狀態圖

Thread類擁有一個System.Threading.ThreadState枚舉類型的字段ThreadState,它包含如下枚舉值:

有關每一個狀態的具體描述能夠在MSDN上一篇名爲「ThreadStateEnumeration」的文章中找到。該枚舉類型是一個二進制位域,這表示一個該類型的實例能夠同時表示多個枚舉值。例如,一個線程能夠同時處於Running、AbortRequested和Background這三種狀態。二進制位域的概念參見10.11.3節。

根據咱們在前面的章節中所瞭解的知識,咱們定義瞭如圖5-1所示的簡化的狀態圖。

圖5-1 簡化的託管線程狀態圖

5.4 訪問資源同步簡介

在多線程應用(一個或多個處理器)的計算中會使用到同步這個詞。實際上,這些應用程序的特色就是它們擁有多個執行單元,而這些單元在訪問資源的時候可能會發生衝突。線程間會共享同步對象,而同步對象的目的在於可以阻塞一個或多個線程,直到另外一個線程使得某個特定條件獲得知足。

咱們將看到,存在多種同步類與同步機制,每種制針對一個或一些特定的需求。若是要利用同步構建一個複雜的多線程應用程序,那麼頗有必要先掌握本章的內容。咱們將在下面的內容中盡力區分他們,尤爲要指出那些在各個機制間最微妙的區別。

合理地同步一個程序是最精細的軟件開發任務之一,單這一個主題就足以寫幾本書。在深刻到細節以前,應該首先確認使用同步是否不可避免。一般,使用一些簡單的規則可讓咱們遠離同步問題。在這些規則中有線程與資源的親緣性規則,咱們將在稍後介紹。

應該意識到,對程序中資源的訪問進行同步時,其難點來自因而使用細粒度鎖仍是粗粒度鎖這個兩難的選擇。若是在訪問資源時採用粗粒度的同步方式,雖然能夠簡化代碼可是也會把本身暴露在爭用瓶頸的問題上。若是粒度過細,代碼又會變的很複雜,以致於維護工做使人生厭。而後又會趕上死鎖和競態條件這些在下面章節將要介紹的問題。

所以在咱們開始談論有關同步機制以前,有必要先了解一下有關競態條件和死鎖的概念。

5.4.1 競態條件

競態條件指的是一種特殊的狀況,在這種狀況下各個執行單元以一種沒有邏輯的順序執行動做,從而致使意想不到的結果。

舉一個例子,線程T修改資源R後,釋放了它對R的寫訪問權,以後又從新奪回R的讀訪問權再使用它,並覺得它的狀態仍然保持在它釋放它以後的狀態。可是在寫訪問權釋放後到從新奪回讀訪問權的這段時間間隔中,可能另外一個線程已經修改了R的狀態。

另外一個經典的競態條件的例子就是生產者/消費者模型。生產者一般使用同一個物理內存空間保存被生產的信息。通常說來,咱們不會忘記在生產者與消費者的併發訪問之間保護這個空間。容易被咱們忘記的是生產者必須確保在生產新信息前,舊的信息已被消費者所讀取。若是咱們沒有采起相應的預防措施,咱們將面臨生產的信息從未被消費的危險。

若是靜態條件沒有被妥善的管理,將致使安全系統的漏洞。同一個應用程序的另外一個實例極可能會引起一系列開發者所預計不到的事件。通常來講,必須對那種用於確認身份鑑別結果的布爾量的寫訪問作最完善的保護。若是沒有這麼作,那麼在它的狀態被身份鑑別機制設置後,到它被讀取以保護對資源的訪問的這段時間內,頗有可能已經被修改了。已知的安全漏洞不少都歸咎於對靜態條件不恰當的管理。其中之一甚至影響了Unix操做系統的內核。

5.4.2 死鎖

死鎖指的是因爲兩個或多個執行單元之間相互等待對方結束而引發阻塞的狀況。例如:

一個線程T1得到了對資源R1的訪問權。

一個線程T2得到了對資源R2的訪問權。

T1請求對R2的訪問權可是因爲此權力被T2所佔而不得不等待。

T2請求對R1的訪問權可是因爲此權力被T1所佔而不得不等待。

T1和T2將永遠維持等待狀態,此時咱們陷入了死鎖的處境!這種問題比你所遇到的大多數的bug都要隱祕,針對此問題主要有三種解決方案:

  • 在同一時刻不容許一個線程訪問多個資源。
  • 爲資源訪問權的獲取定義一個關係順序。換句話說,當一個線程已經得到了R1的訪問權後,將沒法得到R2的訪問權。固然,訪問權的釋放必須遵循相反的順序。
  • 爲全部訪問資源的請求系統地定義一個最大等待時間(超時時間),並妥善處理請求失敗的狀況。幾乎全部的.NET的同步機制都提供了這個功能。

前兩種技術效率更高可是也更加難於實現。事實上,它們都須要很強的約束,而這點隨着應用程序的演變將愈來愈難以維護。儘管如此,使用這些技術不會存在失敗的狀況。

大的項目一般使用第三種方法。事實上,若是項目很大,通常來講它會使用大量的資源。在這種狀況下,資源之間發生衝突的機率很低,也就意味着失敗的狀況會比較罕見。咱們認爲這是一種樂觀的方法。秉着一樣的精神,咱們在19.5節描述了一種樂觀的數據庫訪問模型。

5.5 使用volatile字段與Interlocked類實現同步

5.5.1 volatile字段

volatile字段能夠被多個線程訪問。咱們假設這些訪問沒有作任何同步。在這種狀況下,CLR中一些用於管理代碼和內存的內部機制將負責同步工做,可是此時不能確保對該字段讀訪問總能讀取到最新的值,而聲明爲volatile的字段則能提供這樣的保證。在C#中,若是一個字段在它的聲明前使用了volatile關鍵字,則該字段被聲明爲volatile。

不是全部的字段均可以成爲volatile,成爲這種類型的字段有一個條件。若是一個字段要成爲volatile,它的類型必須是如下所列的類型中的一種:

  • 引用類型(這裏只有訪問該類型的引用是同步的,訪問其成員並不一樣步)。
  • 一個指針(在不安全的代碼塊中)。
  • sbyte、byte、short、ushort、int、uint、char、float、bool(工做在64位處理器上時爲double、long與ulong)。
  • 一個使用如下底層類型的枚舉類型:byte、sbyte、short、ushort、int、uint(工做在64位的處理器上時爲double、long與ulong)。

你可能已經注意到了,只有值或者引用的位數不超過本機整型值的位數(4或8由底層處理器決定)的類型才能成爲volatile。這意味着對更大的值類型進行併發訪問必須進行同步,下面咱們將會對此進行討論。

5.5.2 System.Threading.Interlocked類

經驗顯示,那些須要在多線程狀況下被保護的資源一般是整型值,而這些被共享的整型值最多見的操做就是增長/減小以及相加。.NETFramework利用System.Threading.Interlocked類提供了一個專門的機制用於完成這些特定的操做。這個類提供了Increment()、Decrement()與Add()三個靜態方法,分別用於對int或者long類型變量的遞增、遞減與相加操做,這些變量以引用方式做爲參數傳入。咱們認爲使用Interlocked類讓這些操做具備了原子性。

下面的程序顯示了兩個線程如何併發訪問一個名爲counter的整型變量。一個線程將其遞增5次,另外一個將其遞減5次。

例5-5

該程序輸出(以非肯定方式輸出,意味着每執行一次顯示的結果都是不一樣的):

若是咱們不讓這些線程在每次修改變量後休眠10毫秒,那麼它們將有足夠的時間在一個時間片中完成它們的任務,那樣也就不會出現交叉操做,更不用說併發訪問了。

5.5.3 Interlocked類提供的其餘功能

Interlocked類還容許使用Exchange()靜態方法,以原子操做的形式交換某些變量的狀態。還可使用CompareExchange()靜態方法在知足一個特定條件的基礎上以原子操做的形式交換兩個值。

5.6 使用System.Threading.Monitor類與C#的lock關鍵字實現同步

以原子操做的方式完成簡單的操做無疑是很重要的,可是這還遠不能涵蓋全部須要用到同步的事例。System.Threading.Monitor類幾乎容許將任意一段代碼設置爲在某個時間僅能被一個線程執行。咱們將這段代碼稱之爲臨界區。

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

Monitor類提供了Enter(object)與Exit(object)這兩個靜態方法。這兩個方法以一個對象做爲參數,該對象提供了一個簡單的方式用於惟一標識那個將以同步方式訪問的資源。當一個線程調用了Enter()方法,它將等待以得到訪問該引用對象的獨佔權(僅當另外一個線程擁有該權力的時候它纔會等待)。一旦該權力被得到並使用,線程能夠對同一個對象調用Exit()方法以釋放該權力。

一個線程能夠對同一個對象屢次調用Enter(),只要對同一對象調用相同次數的Exit()來釋放獨佔訪問權。

一個線程也能夠在同一時間擁有多個對象的獨佔權,可是這樣會產生死鎖的狀況。

毫不能對一個值類型的實例調用Enter()與Exit()方法。

無論發生了什麼,必須在finally子句中調用Exit()以釋放全部的獨佔訪問權。

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

例5-6[1]

人們很容易想到用counter來代替typeof(Program),可是counter是一個值類型的靜態成員。須要注意平方和倍增操做是不知足交換律的,因此counter的最終結果是非肯定性的。

5.6.2 C#的lock關鍵字

C#語言經過lock關鍵字提供了一種比使用Enter()和Exit()方法更加簡潔的選擇。咱們的程序能夠改寫爲下面這個樣子:

例5-7

和for以及if關鍵字同樣,若是被lock關鍵字定義的塊僅包含一條指令,就再也不須要花括號。咱們能夠再次改寫爲:

使用lock關鍵字將引導C#編譯器建立出相應的try/finally塊,這樣仍舊能夠預期到任何可能引起的異常。可使用Reflector或者ildasm.exe工具驗證這一點。

5.6.3 SyncRoot模式

和前面的例子同樣,咱們一般在一個靜態方法中使用Monitor類配合一個Type類的實例。一樣,咱們每每會在一個非靜態方法中使用this關鍵字來實現同步。在兩種狀況下,咱們都是經過一個在類外部可見的對象對自身進行同步。若是其餘部分的代碼也利用這些對象來實現自身的同步,就會出現問題。爲了不這種潛在的問題,咱們推薦使用一個類型爲object的名爲SyncRoot的私有成員,至於該成員是靜態的仍是非靜態的則由須要而定。

例5-8

System.Collections.ICollection接口提供了object類型的SyncRoot{get;}屬性。大多數的集合類(泛型或非泛型)都實現了該接口。一樣地,可使用該屬性同步對集合中元素的訪問。不過在這裏SyncRoot模式並無被真正的應用,由於咱們對訪問進行同步所使用對象不是私有的。

例5-9

5.6.4 線程安全類

若一個類的每一個實例在同一時間不能被一個以上的線程所訪問,則該類稱之爲一個線程安全的類。爲了建立一個線程安全的類,只需將咱們見過的SyncRoot模式應用於它所包含的方法。若是一個類想變成線程安全的,而又不想爲類中代碼增長過多負擔,那麼有一個好方法就是像下面這樣爲其提供一個通過線程安全包裝的繼承類。

例5-10

另外一種方法就是使用System.Runtime.Remoting.Contexts.SynchronizationAttribute,這點咱們將在本章稍後討論。

5.6.5 Monitor.TryEnter()方法

該方法與Enter()類似,只不過它是非阻塞的。若是資源的獨佔訪問權已經被另外一個線程佔據,該方法將當即返回一個false返回值。咱們也能夠調用TryEnter()方法,讓它以毫秒爲單位阻塞一段有限的時間。由於該方法的返回結果並不肯定,而且當得到獨佔訪問權後必須在finally子句中釋放該權力,因此建議當TryEnter()失敗時當即退出正在調用的函數:

例5-11[2]

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

Wait()、Pulse()與PulseAll()方法必須在一塊兒使用而且須要結合一個小場景才能被正確理解。咱們的想法是這樣的:一個線程得到了某個對象的獨佔訪問權,而它決定等待(經過調用Wait())直到該對象的狀態發生變化。爲此,該線程必須暫時失去對象獨佔訪問權,以便讓另外一個線程修改對象的狀態。修改對象狀態的線程必須使用Pulse()方法通知那個等待線程修改完成。下面有一個小場景具體說明了這一狀況。

  • 擁有OBJ對象獨佔訪問權的T1線程,調用Wait(OBJ)方法將它本身註冊到OBJ對象的被動等待列表中。
  • 因爲以上的調用,T1失去了對OBJ的獨佔訪問權。所以,另外一個線程T2經過調用Enter(OBJ)得到OBJ的獨佔訪問權。
  • T2最終修改了OBJ的狀態並調用Pulse(OBJ)通知了此次修改。該調用將致使OBJ被動等待列表中的第一個線程(在這裏是T1)被移到OBJ的主動等待列表的首位。而一旦OBJ的獨佔訪問權被釋放,OBJ主動等待列表中的第一個線程將被確保得到該權力。而後它就從Wait(OBJ)方法中退出等待狀態。
  • 在咱們的場景中,T2調用Exit(OBJ)以釋放對OBJ的獨佔訪問權,接着T1恢復訪問權並從Wait(OBJ)方法中退出。
  • PulseAll()將使得被動等待列表中的線程所有轉移到主動等待列表中。注意這些線程將按照它們調用Wait()的順序到達非阻塞態。

若是Wait(OBJ)被一個調用了屢次Enter(OBJ)的線程所調用,那麼該線程將須要調用相同次數的Exit(OBJ)以釋放對OBJ的訪問權。即便在這種狀況下,另外一個線程調用一次Pulse(OBJ)就足以將第一個線程變成非阻塞態。

下面的程序經過ping與pong兩個線程以交替的方式使用一個ball對象的訪問權來演示該功能。

例5-12

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

pong線程沒有結束而且仍然阻塞在Wait()方法上。因爲pong線程是第二個得到ball對象的獨佔訪問權的,因此才致使了該結果。

相關文章
相關標籤/搜索