表1. 各種鎖在Intel Core i7 860的成本比較
建構方式 | .Net Framework版本 | 目的 | 是否可跨Process | 成本 |
lock(Monitor.Enter/Monitor/Exit) | 2.0 | 確保同一時間內僅有一條執行緒可以存取資源。 | 不 | 20ns |
Mutex | 2.0 | 確保同一時間內僅有一條執行緒可以存取資源。 | 可 | 1000ns |
SemaphoreSlim | 4.0 | 確保同一時間內僅有指定的執行緒個數可以存取資源。 | 不 | 200ns |
Semaphore | 2.0 | 確保同一時間內僅有指定的執行緒個數可以存取資源。 | 可 | 1000ns |
ReaderWriterLockSlim | 3.5 | 允許多個讀取型執行緒,但僅有一個寫入型執行緒可以存取資源。 | 不 | 40ns |
ReaderWriterLock(棄用) | 2.0 | 允許多個讀取型執行緒,但僅有一個寫入型執行緒可以存取資源。 | 不 | 100ns |
Lock
Lock是一種物件鎖,因此,它所鎖定的只能是一個參考型變數,不能是值類型的變數。當有一個執行緒取得該物件鎖則在它之後的其它執行緒只能等待第一個執行緒釋放物件鎖。在使用上要注意不同的執行緒是否是將同一個物件來看待成鎖;譬如說,在一段Script Block中宣告了一個物件並且針對該物件作為鎖,這個情境是沒有同步資源存取的能力,因為每個執行執行到這段Script Block後就會自行建立物件並且取得物件鎖。 在實務上,通常會挑選類別中private static Object lockObj作為物件鎖的最佳對像;因為它可以確保它是全域中獨一無二的物件,在使用上比較能夠避開誤用物件當成鎖的對像的狀況。
有三種避免作為物件鎖的物件:
1. lock(this)
2. lock(typeof(SomeType))
3. lock("SomeString")
這三種都有可能發生與預期的鎖定效果有出入的狀況。大多數的集合物件都會提供一個作為物件鎖的內部變數: _syncRoot。但是此一成員在.Net Framework 3.0之後已不再提供給開發人員使用了。
class SomeClass { private static object lockObj = new object(); public void DoSomething() { lock (lockObj) { //工作 } } }
Mutex
其作用為一個同步鎖,用來進行兩個工作對同一個資源存取的控制,藉以避免誤動作或是結果錯誤。Mutex和Monitor很類似只有擁有Mutex物件的執行緒才具有存取資源的權限,由於Mutex物件僅能有一個,也因此確立了資源不會同時被多個執行緒所存取;只有在擁有資源存取權的執行緒發出釋放Mutex訊號之後,其它執行緒才能存取資源。
Mutex較Monitor複雜的地方是在於Mutex允許跨執行元進行資源的鎖定。Mutex在內部是呼叫作業系統的API,也因此它可以跨執行元更也因此它消耗的資源更多。Mutex在鎖定時像是Lock,亦即Monitor.Enter()方法而不是Monitor.Wait()。
Mutex的方法:
1. WaitOne(): 請求資源存取權,會一直持有鎖定到Mutex收到釋放訊號為止。
2. ReleaseMutex(): 釋放Mutex。Mutex的內部計數器由CLR維護,每呼叫WaitOne()一次則Mutext計數器+1,當呼叫ReleaseMutex()便將計數器-1。只要計數不為0,等待資源的執行緒就會繼續等待。
Mutex最糟的使用情境就是擁有Mutex物件鎖的執行緒執行工作到一半便自行結束,且結束前沒有對Mutex物件發出釋放訊號,這種狀況是最惡劣的使用Mutex物件情境,故使用Mutex時必定要是Try/Finally中使用,並且在Finally中對Mutex物件發出釋放訊號。
Mutex的建構子有五個重載,而每一個建構子有著不同的操作目的:
1. Mutex(): 無法進行跨執行元進行資源鎖定,亦為局部(Local)型同步鎖。
2. Mutex(bool): 與1相同,僅能作為局域型同步鎖,而輸入參數是指示實體化Mutex的執行緒是否在實體化後馬上取得同步鎖。
3. Mutex(bool, string): 與2.不同之處在於3.可以跨執行元進行資源鎖定,亦即全域型同步鎖,但是不建議採用這一種方式,因為無法得知是否這個名稱的Mutex物件已在其它執行元建立過並且被取走同步鎖。名稱具大小寫敏感。
4. Mutex(bool, string, out bool): 第三個輸入參數用來指示是否取得同步鎖,是實務中較常用到的。
5. Mutex(bool, string, out bool, MutexSecurity): 最後一個參數是用來進行帳戶安全性控制;控制僅有某幾個帳戶能夠存取某個名稱的Mutex物件的同步鎖。
bool blLock; Mutex mutex = new Mutex( true, @"Global\MyAPP", out blLock); try { if(blLock) //工作 else //等待同步鎖 } finally { mutex.ReleaseMutex(); }
Semaphore/SemaphoreSlim
Semaphore與Mutex類似,但Semaphore可以允許資源給予多個執行緒存取;Semaphore類似一個提供計數的Mutex,可定義執行緒存取個數。同樣地,盡量在Try/Finally中使用Semaphore。
當某些資源是限定僅有幾個執行緒存取時,可以考慮使用Semaphore;譬如,現在僅有3個Port可以使用,而這個時候就以設定Semaphore計數為3,而第四位執行緒就需等待。
在.Net Framework 4.0中提供了Semaphore的輕量級版本-SemaphoreSlim。SemaphoreSlim不具有Semaphore可以跨執行元的能力,故所需資源較Semaphore少。Semaphore同Mutex都是在建構子中指定名稱以作為全域型同步鎖,而名稱同Mutex是大小寫敏感。
Semaphore從機制上來說較類似於Mutex一樣是一種鎖,而不是"通知"。因為Semaphore允許多個執行緒存取同一個資源,因此,Semaphore本身是執行緒非關的,亦即非Semaphore擁有者可以發出釋放訊號,並且可以設定釋放幾個鎖: Release(N),但是當指定釋放的鎖的個數超過Semaphore中所設定的會拋出SemaphoreFullException例外。
Semaphore semaphore = new Semaphore(1, 3); semaphore.WaitOne(); //執行工作 semaphore.Release();
ReaderWriterLockSlim/ReaderWriterLock
和Monitor相同是.Net所撰寫的原生類別,因此在底層並不會去呼叫到OS的API。ReaderWriterLock在運作上是將讀取資源與寫入資源拆分開來看;當執行緒的工作可以被明顯區分成讀取與寫入兩類並且寫入的工作既短且快時ReaderWriterLock是相當好的同步鎖。
ReaderWriterLock本質上仍是同一時間僅能只有"一組"執行緒存取資源;當寫入組取得同步鎖時,讀取組僅能等待寫入組釋放同步鎖,但是較特別的是寫入組一次僅能有一個執行緒取得同步鎖,但是讀取組可以有多個執行緒同時讀取資源。
ReaderWriterLock在處理不當時非常容易造成"飢餓現象";這是因為若寫入組占用太長時間或是兩組中有某個執行緒忘了釋放資源,這是就會造成另一組執行緒永遠無法存取資源,通常會採用時間限制,若在時限內沒有取得同步鎖時便拋出ApplicationException異常。
ReaderWriterLock不允許一個執行緒同時擔任寫入與讀取組,ReaderWriterLock常用方法:
1. AcquireReaderLock(): 請求讀取同步鎖。
2. AcquireWriterLock(): 請求寫入同步鎖。
3. ReleaseReaderLock(): 讀取鎖-1;當讀取鎖為0時,便換寫入組取得資源存取權。
4. ReleaseWriterLock(): 釋放寫入鎖。
5. ReleaseLock(): 釋放鎖;不論當前是處於計數寫入或是讀取。
EventWaitHandler一族
EventWaitHandler在應用上都是以"通知"為主軸;在某些情境下希望執行緒在工作完成之後能夠通知已完成,讓整個系統的運行更順暢而不需要以輪詢(Pooling)這種低效能的作法判斷是否執行緒已經完成工作。
EventWaitHandler僅有通知能力本身並不具有資源鎖定的能力,因此,在實務中通常會搭配Lock/Monitor/Mutex一起實做平行系統;共有兩組執行緒,一組擔任生產者,另一組則是擔任消費者並且由Lock/Monitor/Mutex進行資源的鎖定,開始時僅有生產者開始工作另一組消費者則是被阻擋,當生產者完工之後會通知消費者進行消費並且自身進入阻擋狀態,待消費者組完成工作後再通知生產者工作,如此往復循環。
EventWaitHandle有以下幾個常用的方法:
1. SingnalAndWait(): EventWaitHandle的靜態方法;通知另一個EventWaitHandle並且自身進入等待狀態
2. WaitAny(): EventWaitHandle的靜態方法;等待一組EventWaitHandle中某個EventWaitHandle進入執行狀態
3. WaitAll(): EventWaitHandle的靜態方法;等待一組執行緒內所有EventWaitHandle進入執行狀態
4. Set(): 進入執行狀態
5. Reset(): 進入等待狀態
EventWaitHandle的建構子不同重載具不同能力:
1. EventWaitHandle(bool, EventResetMode, String): 第三個參數用在全域型控制時使用,而第二個參數有: AutoReset/ManualReset兩個列舉值;AutoReset是當呼叫Set()之後釋放處於等待的一個執行緒後自動進入等待狀態,其餘執行緒依舊處於等待,ManualReset會釋放所有等待中的執行緒,並在手動呼叫Reset()前,一直處於通行狀態。
2. EventWaitHandle(bool ,EventResetMode ,string ,out bool ,EventWaitHandleSecurity): 後面兩參數同Mutex。
EventWaitHandle的子類別: AutoResetEvent/ManualResetEvent,其特性就如同EventResetMod一般。
AutoResetEvent(自動重置事件)
在AutoResetEvent的建構子: public AutoResetEvent(bool InitialState),在建構子中使用bool型別的輸入參數來作為初始狀態的設定;若要將AutoResetEvent的初始狀態設定為:
1. 終止: 輸入值為true
2. 非終止: 輸入值為false
一般來說,AutoResetEvent常用到的方法有兩個:
1. WaitOne(int millisecondsTimeout): 這個方法是用來讓呼叫它的執行緒進入等待狀態,並且依傳入的參數作為等待秒數,當秒數逾時後原本進入等待狀態的執行緒會自動恢復成執行狀態,而逾時後會回傳false。
執行緒透過呼叫WaitOne這個方法來讓自己進入等待狀態,但是否進入等待狀態還要依AutoResetEvent本身是處於那種初始狀態(i.e. 終止or非終止);若AutoResetEvent本身是處於非終止狀態則執行緒呼叫WaitOne時會進入等待狀態,但AutoResetEvent為終止狀態時呼叫WaitOne卻不會進入等待狀態,但是AutoResetEvent會自動轉變成為非終止狀態,當再次呼叫WaitOne時就能夠讓執行緒進入等待狀態。
2. Signal(): 這個方法是用來讓某一個因呼叫WaitOne而進入等待狀態的執行緒,從等待狀態恢復到執行狀態。
public static AutoResetEvent autoEvent = new AutoResetEvent(false); public static void Main(string[] args) { Console.WriteLine("Main thread Start at:" + DateTime.Now.ToLongTimeString()); Thread t = new Thread(TestMethod); t.Start(); Thread.Sleep(3000); autoEvent.Set(); Console.Read(); } public static void TestMethod() { if(autoEvent.WaitOne(2000)) { Console.WriteLine("Get Signal to Work"); Console.WriteLine("Method restart run at:" + DateTime.Now.ToLongTimeString()); } else { Console.WriteLine("Time out to work"); Conosle.WriteLine("Method restart run at:" + DateTime.Now.ToLongTimeString()); } }
ManualResetEvent(手動重置事件)
ManualResetEvent和AutoResetEvent使用上幾乎一致,因為它們都是衍生自EventWaitHandle。不過其區別如下:
1. 當AutoResetEvent為終止狀態時,呼叫WaitOne方法的執行緒不會進入等待狀態,而AutoResetEvent則本身會將狀態改變成為非終止狀態。
2. ManualResetEvent為終止狀態時呼叫WaitOne方法的執行緒不會進入等待狀態,ManualResetEvent也不會自動進入非終止狀態。
private static ManualResetEvent manualEvent = new ManualResetEvent(true); public static void Main(string[] args) { Console.WriteLine("Main Thread Start run at:" + DateTime.Now.ToLongTimeString()); Thread t = new Thread(TestMethod); t.Start(); Console.Read(); } public static void TestMethod() { manualEvent.WaitOne(); Console.WriteLine("Method start at:" + DateTime.Now.ToLongTimeString()); manualEvent.WaitOne(1000); Console.WriteLine("Method start at:" + DateTime.Now.ToLongTimeString()); } ManualResetEvent
完全不會阻擋,縱使設定了逾時時間也一樣。
沒有留言:
張貼留言