進程和線程

簡單的說一個程序就是一個進程,而一個程序中的多個任務則被稱爲線程。 
word是一個進程,而其中的一個文檔就是一個線程!


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

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

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

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被用戶所有。

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

  • Windows是按需爲每個進程分配內存的,4GB是32位系統中一個進程所佔空間的上限。

  • 將進程所需的內存劃分爲4KB大小的內存頁,並根據使用情況將這些內存頁存儲在硬盤上或加載到RAM中,通過系統的這種虛擬內存機制,我們可以有效地減少對實際內存的需求量。當然這些對用戶和開發者來說都是透明的。

5.2.2 System.Diagnostics.Process類

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

  • 該實例的當前進程。

  • 本機上除了當前進程的其他進程。

  • 遠程機器上的某個進程。

通過該類所包含的方法和字段,可以創建或銷燬一個進程,並且可以獲得一個進程的相關信息。下面將討論一些使用該類實現的常見任務。

5.2.3 創建和銷燬子進程

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

例5-1

clip_p_w_picpath002_7.jpg

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

clip_p_w_picpath006.jpg

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

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

有些應用程序需要這種功能。實際上,通常來說在同一臺機器上同時運行一個應用程序的多個實例並沒有意義。

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

  • 該技術具有使互斥體的名字被其他應用程序所使用的較小的、潛在的風險。在這種情況下該技術將不再有效並且會造成很難檢測到的bug。

  • 該技術不能解決我們僅允許一個應用程序產生N個實例這種一般的問題。

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

例5-2

clip_p_w_picpath008.jpg

通過方法參數指定了遠程機器的名字後,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枚舉包含以下值:

clip_p_w_picpath002_5.gif

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

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

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

2. 線程的優先級

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

clip_p_w_picpath002_10.gif

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

5.3.5 System.Threading.Thread類

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

clip_p_w_picpath014.jpg

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

clip_p_w_picpath016.jpg

5.3.6 創建與Join一個線程

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

例5-3

clip_p_w_picpath018.jpg

該程序輸出:

clip_p_w_picpath020.jpg

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

5.3.7 掛起一個線程

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

clip_p_w_picpath022.jpg

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

5.3.8 終止一個線程

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

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

  • 被自己終止。

  • 被另一個線程終止。

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

例5-4 主線程的自殺

clip_p_w_picpath024.jpg

當線程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,它包含以下枚舉值:

clip_p_w_picpath026.jpg

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

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

clip_p_w_picpath028.jpg

圖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

clip_p_w_picpath030.jpg

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

clip_p_w_picpath032.jpg

clip_p_w_picpath033.jpg

如果我們不讓這些線程在每次修改變量後休眠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()方法以釋放該權力。

clip_p_w_picpath002_1.jpg一個線程可以對同一個對象多次調用Enter(),只要對同一對象調用相同次數的Exit()來釋放獨佔訪問權。

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

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

不管發生了什麼,必須在finally子句中調用Exit()以釋放所有的獨佔訪問權。

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

例5-6[1]

clip_p_w_picpath034.gifclip_p_w_picpath036.jpg

clip_p_w_picpath037.jpg

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

5.6.2 C#的lock關鍵字

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

例5-7

clip_p_w_picpath039.jpg

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

clip_p_w_picpath041.jpg

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

5.6.3 SyncRoot模式

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

例5-8

clip_p_w_picpath043.jpg

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

例5-9

clip_p_w_picpath045.jpg

5.6.4 線程安全類

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

例5-10

clip_p_w_picpath047.jpg

clip_p_w_picpath048.jpg

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

5.6.5 Monitor.TryEnter()方法

clip_p_w_picpath050.jpg

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

例5-11[2]

clip_p_w_picpath052.jpg

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

clip_p_w_picpath055.jpg

clip_p_w_picpath056.jpg

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()的順序到達非阻塞態。

clip_p_w_picpath002_2.jpg如果Wait(OBJ)被一個調用了多次Enter(OBJ)的線程所調用,那麼該線程將需要調用相同次數的Exit(OBJ)以釋放對OBJ的訪問權。即使在這種情況下,另一個線程調用一次Pulse(OBJ)就足以將第一個線程變成非阻塞態。

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

例5-12

clip_p_w_picpath058.jpg

clip_p_w_picpath060.jpg

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

clip_p_w_picpath062.jpg

pong線程沒有結束並且仍然阻塞在Wait()方法上。由於pong線程是第二個獲得ball對象的獨佔訪問權的,所以才導致了該結果。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章