.NET多執行緒程式設計(3):執行緒同步 (轉)

worldblog發表於2007-12-14
.NET多執行緒程式設計(3):執行緒同步 (轉)[@more@]

多執行緒(3):執行緒同步:namespace prefix = o ns = "urn:schemas--com::office" />

隨著對多執行緒學習的深入,你可能覺得需要了解一些有關執行緒共享資源的問題. 提供了很多的類和資料型別來控制對共享資源的訪問。

考慮一種我們經常遇到的情況:有一些全域性變數和共享的類變數,我們需要從不同的執行緒來它們,可以透過使用System.Threading.Interlocked類完成這樣的任務,它提供了原子的,非模組化的整數更新操作。

還有你可以使用System.Threading.Monitor類鎖定的方法的一段程式碼,使其暫時不能被別的執行緒訪問。

System.Threading.WaitHandle類的例項可以用來封裝等待對共享資源的獨佔訪問權的操作特定的物件。尤其對於非受管程式碼的互操作問題。

System.Threading.Mutex用於對多個複雜的執行緒同步的問題,它也允許單執行緒的訪問。

ManualResetEventAutoResetEvent這樣的同步事件類支援一個類通知其他事件的執行緒。

不討論執行緒的同步問題,等於對多執行緒程式設計知之甚少,但是我們要十分謹慎的使用多執行緒的同步。在使用執行緒同步時,我們事先就要要能夠正確的確定是那個物件和方法有可能造成死鎖(死鎖就是所有的執行緒都停止了相應,都在等者對方釋放資源)。還有贓資料的問題(指的是同一時間多個執行緒對資料作了操作而造成的不一致),這個不容易理解,這麼說吧,有X和Y兩個執行緒,執行緒X從讀取資料並且寫資料到資料結構,執行緒Y從這個資料結構讀資料並將資料送到其他的。假設在Y讀資料的同時,X寫入資料,那麼顯然Y讀取的資料與實際的資料是不一致的。這種情況顯然是我們應該避免發生的。少量的執行緒將使得剛才的問題發生的機率要少的多,對共享資源的訪問也更好的同步。

.NET Framework的CLR提供了三種方法來完成對共享資源 ,諸如全域性變數域,特定的程式碼段,靜態的和例項化的方法和域。

(1)  程式碼域同步:使用Monitor類可以同步靜態/例項化的方法的全部程式碼或者部分程式碼段。不支援靜態域的同步。在例項化的方法中,this指標用於同步;而在靜態的方法中,類用於同步,這在後面會講到。

(2)  手工同步:使用不同的同步類(諸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent, AutoResetEvent 和Interlocked等)建立自己的同步機制。這種同步方式要求你自己手動的為不同的域和方法同步,這種同步方式也可以用於程式間的同步和對共享資源的等待而造成的死鎖解除。

(3)  上下文同步:使用SynchronizationAttribute為ContextBound物件建立簡單的,自動的同步。這種同步方式僅用於例項化的方法和域的同步。所有在同一個上下文域的物件共享同一個鎖。

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)受到了Pulse或PulseAll,等待佇列的執行緒就進入就緒佇列。執行緒X重新得到物件鎖時,Monitor.Wait才返回。如果擁有鎖的執行緒(執行緒Y)不呼叫Pulse或PulseAll,方法可能被不確定的鎖定。Pulse, PulseAll and Wait必須是被同步的程式碼段鄂被呼叫。對每一個同步的物件,你需要有當前擁有鎖的執行緒的指標,就緒佇列和等待佇列(包含需要被通知鎖定物件的狀態變化的執行緒)的指標。

你也許會問,當兩個執行緒同時呼叫Monitor.Enter會發生什麼事情?無論這兩個執行緒地呼叫Monitor.Enter是多麼地接近,實際上肯定有一個在前,一個在後,因此永遠只會有一個獲得物件鎖。既然Monitor.Enter是原子操作,那麼是不可能偏好一個執行緒而不喜歡另外一個執行緒的。為了獲取更好的,你應該延遲後一個執行緒的獲取鎖呼叫和立即釋放前一個執行緒的物件鎖。對於private和internal的物件,加鎖是可行的,但是對於external物件有可能導致死鎖,因為不相關的程式碼可能因為不同的目的而對同一個物件加鎖。

如果你要對一段程式碼加鎖,最好的是在try語句裡面加入設定鎖的語句,而將Monitor.Exit放在finally語句裡面。對於整個程式碼段的加鎖,你可以使用MethodImplAttribute(在System.Runtime.CompilerServices名稱空間)類在其構造器中設定同步值。這是一種可以替代的方法,當加鎖的方法返回時,鎖也就被釋放了。如果需要要很快釋放鎖,你可以使用Monitor類和 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.Enter和Monitoy.Exit同樣的功能,這種方法用在你的程式碼段不能被其他獨立的執行緒中斷的情況。

WaitHandle Class

WaitHandle類作為基類來使用的,它允許多個等待操作。這個類封裝了的同步處理方法。WaitHandle物件通知其他的執行緒它需要對資源排他性的訪問,其他的執行緒必須等待,直到WaitHandle不再使用資源和等待控制程式碼沒有被使用。下面是從它繼承來的幾個類:

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

:通知一個或多個正在等待的執行緒已發生事件。無法繼承此類。

:當通知一個或多個正在等待的執行緒事件已發生時出現。無法繼承此類。

這些類定義了一些訊號機制使得對資源排他性訪問的佔有和釋放。他們有兩種狀態:signaled 和 nonsignaled。Signaled狀態的等待控制程式碼不屬於任何執行緒,除非是nonsignaled狀態。擁有等待控制程式碼的執行緒不再使用等待控制程式碼時用set方法,其他的執行緒可以呼叫Reset方法來改變狀態或者任意一個WaitHandle方法要求擁有等待控制程式碼,這些方法見下面:

:等待指定陣列中的所有元素收到訊號。

:等待指定陣列中的任一元素收到訊號。

:當在派生類中重寫時,阻塞當前執行緒,直到當前的 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,等待佇列的下一個執行緒將會得到Mutex。Mutex類對應與win32的CreateMutex,建立Mutex物件的方法非常簡單,常用的有下面幾種方法:

一個執行緒可以透過呼叫WaitHandle.WaitOne 或 WaitHandle.WaitAny 或 WaitHandle.WaitAll得到Mutex的擁有權。如果Mutex不屬於任何執行緒,上述呼叫將使得執行緒擁有Mutex,而且WaitOne會立即返回。但是如果有其他的執行緒擁有Mutex,WaitOne將陷入無限期的等待直到獲取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 nonsignaled。AutoResetEvent 和 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來增加或減少共享變數.它的有點在於是原子操作,也就是說這些方法可以代一個整型的引數增量並且返回新的值,所有的操作就是一步.你也可以使用它來指定變數的值或者檢查兩個變數是否相等,如果相等,將用指定的值代替其中一個變數的值.

ReaderWriterLock class

它定義了一種鎖,提供唯一寫/多讀的機制,使得讀寫的同步.任意數目的執行緒都可以讀資料,資料鎖在有執行緒更新資料時將是需要的.讀的執行緒可以獲取鎖,當且僅當這裡沒有寫的執行緒.當沒有讀執行緒和其他的寫執行緒時,寫執行緒可以得到鎖.因此,一旦writer-lock被請求,所有的讀執行緒將不能讀取資料直到寫執行緒訪問完畢.它支援暫停而避免死鎖.它也支援巢狀的讀/寫鎖.支援巢狀的讀鎖的方法是ReaderWriterLock.AcquireReaderLock,如果一個執行緒有寫鎖則該執行緒將暫停;

支援巢狀的寫鎖的方法是ReaderWriterLock.AcquireWriterLock,如果一個執行緒有讀鎖則該執行緒暫停.如果有讀鎖將容易倒是死鎖.的辦法是使用ReaderWriterLock.UpgradeToWriterLock方法,這將使讀者升級到寫者.你可以用ReaderWriterLock.DowngradeFromWriterLock方法使寫者降級為讀者.呼叫ReaderWriterLock.ReleaseLock將釋放鎖, ReaderWriterLock.RestoreLock將重新裝載鎖的狀態到呼叫ReaderWriterLock.ReleaseLock以前.

結論:

這部分講述了.NET平臺上的執行緒同步的問題.造接下來的系列文章中我將給出一些例子來更進一步的說明這些使用的方法和技巧.雖然執行緒同步的使用會給我們的程式帶來很大的價值,但是我們最好能夠小心使用這些方法.否則帶來的不是受益,而將倒是效能下降甚至程式崩潰.只有大量的聯絡和體會才能使你駕馭這些技巧.儘量少使用那些在同步程式碼塊完成不了或者不確定的阻塞的東西,尤其是I/O操作;儘可能的使用區域性變數來代替全域性變數;同步用在那些部分程式碼被多個執行緒和程式訪問和狀態被不同的程式共享的地方;安排你的程式碼使得每一個資料在一個執行緒裡得到精確的控制;不是共享線上程之間的程式碼是安全的;在下一篇文章中我們將學習執行緒池有關的知識.


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-993473/,如需轉載,請註明出處,否則將追究法律責任。

相關文章