.NET多線程編程(3)——線程同步

    隨着對多線程學習的深刻,你可能以爲須要瞭解一些有關線程共享資源的問題. .NET framework提供了不少的類和數據類型來控制對共享資源的訪問。

  考慮一種咱們常常遇到的狀況:有一些全局變量和共享的類變量,咱們須要從不一樣的線程來更新它們,能夠經過使用System.Threading.Interlocked類完成這樣的任務,它提供了原子的,非模塊化的整數更新操做。

  還有你可使用System.Threading.Monitor類鎖定對象的方法的一段代碼,使其暫時不能被別的線程訪問。

  System.Threading.WaitHandle類的實例能夠用來封裝等待對共享資源的獨佔訪問權的操做系統特定的對象。尤爲對於非受管代碼的互操做問題。

  System.Threading.Mutex用於對多個複雜的線程同步的問題,它也容許單線程的訪問。

  像ManualResetEventAutoResetEvent這樣的同步事件類支持一個類通知其餘事件的線程。

  不討論線程的同步問題,等於對多線程編程知之甚少,可是咱們要十分謹慎的使用多線程的同步。在使用線程同步時,咱們事先就要要可以正確的肯定是那個對象和方法有可能形成死鎖(死鎖就是全部的線程都中止了相應,都在等者對方釋放資源)。還有贓數據的問題(指的是同一時間多個線程對數據做了操做而形成的不一致),這個不容易理解,這麼說吧,有XY兩個線程,線程X從文件讀取數據而且寫數據到數據結構,線程Y從這個數據結構讀數據並將數據送到其餘的計算機。假設在Y讀數據的同時,X寫入數據,那麼顯然Y讀取的數據與實際存儲的數據是不一致的。這種狀況顯然是咱們應該避免發生的。少許的線程將使得剛纔的問題發生的概率要少的多,對共享資源的訪問也更好的同步。

  .NET FrameworkCLR提供了三種方法來完成對共享資源 ,諸如全局變量域,特定的代碼段,靜態的和實例化的方法和域。

  (1 代碼域同步:使用Monitor類能夠同步靜態/實例化的方法的所有代碼或者部分代碼段。不支持靜態域的同步。在實例化的方法中,this指針用於同步;而在靜態的方法中,類用於同步,這在後面會講到。

  (2 手工同步:使用不一樣的同步類(諸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent Interlocked等)建立本身的同步機制。這種同步方式要求你本身手動的爲不一樣的域和方法同步,這種同步方式也能夠用於進程間的同步和對共享資源的等待而形成的死鎖解除。

  (3 上下文同步:使用SynchronizationAttributeContextBoundObject對象建立簡單的,自動的同步。這種同步方式僅用於實例化的方法和域的同步。全部在同一個上下文域的對象共享同一個鎖。
Monitor Class

  在給定的時間和指定的代碼段只能被一個線程訪問,Monitor 類很是適合於這種狀況的線程同步。這個類中的方法都是靜態的,因此不須要實例化這個類。下面一些靜態的方法提供了一種機制用來同步對象的訪問從而避免死鎖和維護數據的一致性。

  Monitor.Enter 方法:在指定對象上獲取排他鎖。

  Monitor.TryEnter 方法:試圖獲取指定對象的排他鎖。

  Monitor.Exit 方法:釋放指定對象上的排他鎖。

  Monitor.Wait 方法:釋放對象上的鎖並阻塞當前線程,直到它從新獲取該鎖。

  Monitor.Pulse 方法:通知等待隊列中的線程鎖定對象狀態的更改。

  Monitor.PulseAll 方法:通知全部的等待線程對象狀態的更改。

  經過對指定對象的加鎖和解鎖能夠同步代碼段的訪問。Monitor.Enter, Monitor.TryEnter Monitor.Exit用來對指定對象的加鎖和解鎖。一旦獲取(調用了Monitor.Enter)指定對象(代碼段)的鎖,其餘的線程都不能獲取該鎖。舉個例子來講吧,線程X得到了一個對象鎖,這個對象鎖能夠釋放的(調用Monitor.Exit(object) or Monitor.Wait)。當這個對象鎖被釋放後,Monitor.Pulse方法和 Monitor.PulseAll方法通知就緒隊列的下一個線程進行和其餘全部就緒隊列的線程將有機會獲取排他鎖。線程X釋放了鎖而線程Y得到了鎖,同時調用Monitor.Wait的線程X進入等待隊列。當從當前鎖定對象的線程(線程Y)受到了PulsePulseAll,等待隊列的線程就進入就緒隊列。線程X從新獲得對象鎖時,Monitor.Wait才返回。若是擁有鎖的線程(線程Y)不調用PulsePulseAll,方法可能被不肯定的鎖定。Pulse, PulseAll and Wait必須是被同步的代碼段鄂被調用。對每個同步的對象,你須要有當前擁有鎖的線程的指針,就緒隊列和等待隊列(包含須要被通知鎖定對象的狀態變化的線程)的指針。

  你也許會問,當兩個線程同時調用Monitor.Enter會發生什麼事情?不管這兩個線程地調用Monitor.Enter是多麼地接近,實際上確定有一個在前,一個在後,所以永遠只會有一個得到對象鎖。既然Monitor.Enter是原子操做,那麼CPU是不可能偏好一個線程而不喜歡另一個線程的。爲了獲取更好的性能,你應該延遲後一個線程的獲取鎖調用和當即釋放前一個線程的對象鎖。對於privateinternal的對象,加鎖是可行的,可是對於external對象有可能致使死鎖,由於不相關的代碼可能由於不一樣的目的而對同一個對象加鎖。

  若是你要對一段代碼加鎖,最好的是在try語句裏面加入設置鎖的語句,而將Monitor.Exit放在finally語句裏面。對於整個代碼段的加鎖,你可使用MethodImplAttribute(在System.Runtime.CompilerServices命名空間)類在其構造器中設置同步值。這是一種能夠替代的方法,當加鎖的方法返回時,鎖也就被釋放了。若是須要要很快釋放鎖,你可使用Monitor類和C# lock的聲明代替上述的方法。

  讓咱們來看一段使用Monitor類的代碼:
public void some_method()
{

int a=100;

int b=0;

Monitor.Enter(this);

//say we do something here.

int c=a/b;

Monitor.Exit(this);

}

  上面的代碼運行會產生問題。當代碼運行到int c=a/b; 的時候,會拋出一個異常,Monitor.Exit將不會返回。所以這段程序將掛起,其餘的線程也將得不到鎖。有兩種方法能夠解決上面的問題。第一個方法是:將代碼放入try…finally內,在finally調用Monitor.Exit,這樣的話最後必定會釋放鎖。第二種方法是:利用C#lock()方法。調用這個方法和調用Monitoy.Enter的做用效果是同樣的。可是這種方法一旦代碼執行超出範圍,釋放鎖將不會自動的發生。見下面的代碼:
public void some_method()
{

int a=100;

int b=0;

lock(this);

//say we do something here.

int c=a/b;

}

  C# lock申明提供了與Monitoy.EnterMonitoy.Exit一樣的功能,這種方法用在你的代碼段不能被其餘獨立的線程中斷的狀況。
WaitHandle Class

  WaitHandle類做爲基類來使用的,它容許多個等待操做。這個類封裝了win32的同步處理方法。WaitHandle對象通知其餘的線程它須要對資源排他性的訪問,其餘的線程必須等待,直到WaitHandle再也不使用資源和等待句柄沒有被使用。下面是從它繼承來的幾個類:

  Mutex 類:同步基元也可用於進程間同步。

  AutoResetEvent:通知一個或多個正在等待的線程已發生事件。沒法繼承此類。

  ManualResetEvent:當通知一個或多個正在等待的線程事件已發生時出現。沒法繼承此類。

  這些類定義了一些信號機制使得對資源排他性訪問的佔有和釋放。他們有兩種狀態:signaled nonsignaledSignaled狀態的等待句柄不屬於任何線程,除非是nonsignaled狀態。擁有等待句柄的線程再也不使用等待句柄時用set方法,其餘的線程能夠調用Reset方法來改變狀態或者任意一個WaitHandle方法要求擁有等待句柄,這些方法見下面:

  WaitAll:等待指定數組中的全部元素收到信號。

  WaitAny:等待指定數組中的任一元素收到信號。

  WaitOne:當在派生類中重寫時,阻塞當前線程,直到當前的 WaitHandle 收到信號。
  這些wait方法阻塞線程直到一個或者更多的同步對象收到信號。

  WaitHandle對象封裝等待對共享資源的獨佔訪問權的操做系統特定的對象不管是收管代碼仍是非受管代碼均可以使用。可是它沒有Monitor使用輕便,Monitor是徹底的受管代碼並且對操做系統資源的使用很是有效率。
Mutex Class
  Mutex是另一種完成線程間和跨進程同步的方法,它同時也提供進程間的同步。它容許一個線程獨佔共享資源的同時阻止其餘線程和進程的訪問。Mutex的名字就很好的說明了它的全部者對資源的排他性的佔有。一旦一個線程擁有了Mutex,想獲得Mutex的其餘線程都將掛起直到佔有線程釋放它。Mutex.ReleaseMutex方法用於釋放Mutex,一個線程能夠屢次調用wait方法來請求同一個Mutex,可是在釋放Mutex的時候必須調用一樣次數的Mutex.ReleaseMutex。若是沒有線程佔有Mutex,那麼Mutex的狀態就變爲signaled,不然爲nosignaled。一旦Mutex的狀態變爲signaled,等待隊列的下一個線程將會獲得MutexMutex類對應與win32CreateMutex,建立Mutex對象的方法很是簡單,經常使用的有下面幾種方法:


  一個線程能夠經過調用WaitHandle.WaitOne WaitHandle.WaitAny WaitHandle.WaitAll獲得Mutex的擁有權。若是Mutex不屬於任何線程,上述調用將使得線程擁有Mutex,並且WaitOne會當即返回。可是若是有其餘的線程擁有MutexWaitOne將陷入無限期的等待直到獲取Mutex。你能夠在WaitOne方法中指定參數即等待的時間而避免無限期的等待Mutex。調用Close做用於Mutex將釋放擁有。一旦Mutex被建立,你能夠經過GetHandle方法得到Mutex的句柄而給WaitHandle.WaitAny WaitHandle.WaitAll 方法使用。

  下面是一個示例:
public void some_method()
{

int a=100;

int b=20;

Mutex firstMutex = new Mutex(false);

FirstMutex.WaitOne();

//some kind of processing can be done here.

Int x=a/b;

FirstMutex.Close();

}
  在上面的例子中,線程建立了Mutex,可是開始並無申明擁有它,經過調用WaitOne方法擁有Mutex
Synchronization Events
  同步時間是一些等待句柄用來通知其餘的線程發生了什麼事情和資源是可用的。他們有兩個狀態:signaled and nonsignaledAutoResetEvent ManualResetEvent就是這種同步事件。
AutoResetEvent Class
  這個類能夠通知一個或多個線程發生事件。當一個等待線程獲得釋放時,它將狀態轉換爲signaled。用set方法使它的實例狀態變爲signaled。可是一旦等待的線程被通知時間變爲signaled,它的轉檯將自動的變爲nonsignaled。若是沒有線程偵聽事件,轉檯將保持爲signaled。此類不能被繼承。
ManualResetEvent Class
  這個類也用來通知一個或多個線程事件發生了。它的狀態能夠手動的被設置和重置。手動重置時間將保持signaled狀態直到ManualResetEvent.Reset設置其狀態爲nonsignaled,或保持狀態爲nonsignaled直到ManualResetEvent.Set設置其狀態爲signaled。這個類不能被繼承。
Interlocked Class
  它提供了在線程之間共享的變量訪問的同步,它的操做時原子操做,且被線程共享.你能夠經過Interlocked.Increment Interlocked.Decrement來增長或減小共享變量.它的有點在因而原子操做,也就是說這些方法能夠代一個整型的參數增量而且返回新的值,全部的操做就是一步.你也可使用它來指定變量的值或者檢查兩個變量是否相等,若是相等,將用指定的值代替其中一個變量的值.
相關文章
相關標籤/搜索