標籤:自動 ack 取資料 hand 代碼 重要 oba 項目 訪問
開篇:
互斥還是lock Monitor Mutex 模式!
Muex Monitor lock AutoEventSet ManualEventSet
後續的
ReaderWriterLock
| ReaderWriterLockSlim 類 表示用於管理資源訪問的鎖定狀態,可實現多線程讀取或進行獨佔式寫入訪問。 使用 ReaderWriterLockSlim 來保護由多個線程讀取但每次只採用一個線程寫入的資源。 ReaderWriterLockSlim 允許多個線程均處於讀模數式,允許一個線程處於寫入模式並獨佔鎖定狀態,同時還允許一個具有讀取許可權的線程處於可升級的讀模數式,在此模式下線程無需放棄對資源的讀取許可權即可升級為寫入模式。 注意 ReaderWriterLockSlim 類似於 ReaderWriterLock,只是簡化了遞迴、升級和降級鎖定狀態的規則。 ReaderWriterLockSlim 可避免多種潛在的死結情況。 此外,ReaderWriterLockSlim 的效能明顯優於 ReaderWriterLock。 建議在所有新的開發工作中使用 ReaderWriterLockSlim。 以上引用自MSDN |
C# 線程手冊 第三章 使用線程 ReaderWriterLock 類
2012-02-07 21:53 by DanielWise, 4166 閱讀, 1 評論, 收藏, 編輯
一個ReaderWriterLock 類定義一個實現單寫多讀語義的鎖。這個類通常用在能被多個線程讀取但是僅能被一個線程寫入的檔案操作時使用。下面是ReaderWriterLock類中的四個主要方法:
a. AcquireReaderLock(): 這個重載方法擷取一個讀者鎖,接受一個整型或者TimeSpan類型的timeout 值。timeout是一個檢測死結的利器。
b. AcquireWriterLock(): 這個重載方法擷取一個寫者鎖,接受一個整型或者TimeSpan類型的timeout 值。
c. ReleaseReaderLock(): 釋放讀者鎖。
d. ReleaseWriterLock(): 釋放寫者鎖。
使用ReaderWriterLock類可以讓多安全執行緒地進行資料並發讀取。只有當線程正在更新的資料鎖定。讀者線程可以再沒有寫者擁有鎖的時候獲得鎖。寫者線程可以再沒有讀者線程或者寫者線程擁有鎖的時候獲得鎖。
下面的列表ReadeWriteLock.cs, 描述了如何使用ReaderWriterLock()鎖:
/*************************************/* copyright (c) 2012 daniel dong * * author:daniel dong * blog: www.cnblogs.com/danielwise * email: [email protected] * */using System;using System.Collections.Generic;using System.Text;using System.Threading;namespace ReadWriteLock{ public class ReadWrite { private ReaderWriterLock rwl; private int x; private int y; public ReadWrite() { rwl = new ReaderWriterLock(); } public void ReadInts(ref int a, ref int b) { rwl.AcquireReaderLock(Timeout.Infinite); try { a = this.x; b = this.y; } finally { rwl.ReleaseReaderLock(); } } public void WriteInts(int a, int b) { rwl.AcquireWriterLock(Timeout.Infinite); try { this.x = a; this.y = b; Console.WriteLine("x = " + this.x + " y = " + this.y + " ThreadID = " + Thread.CurrentThread.GetHashCode()); } finally { rwl.ReleaseWriterLock(); } } } public class RWApp { private ReadWrite rw = new ReadWrite(); public static void Main(string[] args) { RWApp e = new RWApp(); //Writer Threads Thread wt1 = new Thread(new ThreadStart(e.Write)); wt1.Start(); Thread wt2 = new Thread(new ThreadStart(e.Write)); wt2.Start(); //Reader Threads Thread rt1 = new Thread(new ThreadStart(e.Read)); rt1.Start(); Thread rt2 = new Thread(new ThreadStart(e.Read)); rt2.Start(); Console.ReadLine(); } private void Write() { int a = 10; int b = 11; Console.WriteLine("************** Write *************"); for (int i = 0; i < 5; i++) { this.rw.WriteInts(a++, b++); Thread.Sleep(1000); } } private void Read() { int a = 10; int b = 11; Console.WriteLine("************** Read *************"); for (int i = 0; i < 5; i++) { this.rw.ReadInts(ref a, ref b); Console.WriteLine("For i = " + i + " a = " + a + " b = " + b + " TheadID = " + Thread.CurrentThread.GetHashCode()); Thread.Sleep(1000); } } }}
ReadWriteLock 的輸出結果可能與下表類似:
在上面的列表中,線程wt1 和 wt2 是WriteInts()方法中獲得寫鎖的寫者線程,線程rt1 和 rt2 是在ReadInts()方法中獲得讀者鎖的讀者線程。在WriteInts()方法中,變數x 和 y 的值分別被改成a 和 b. 當線程wt1 或 wt2 通過調用AcquireWriterLock() 方法獲得一個寫者鎖後,那麼直到這個線程通過調用ReleaseWriterLock()方法釋放鎖之前任何其他線程(包括讀者線程rt1 和 rt2)都不被允許訪問相應對象。這個行為與Monitors類似 。在ReadInts()方法中,線程rt1 和 rt2 通過調用AcquireReaderLock()方法獲得讀者鎖, 這兩個線程可以並發地訪問變數x 和 y. 直到讀者線程釋放它們的讀者鎖以後,寫者線程(wt1 和 wt2)才被允許訪問對應對象。只有讀者線程在獲得讀者鎖以後才可以並發地訪問。
Monitors類對於只想來讀資料而非寫資料來說過於“安全”了。Monitors 也有一些效能問題,對於唯讀類型的訪問來說,效能瓶頸是可以避免的。ReaderWriterLock類通過允許任意數量的線程並發地讀取資料來提供一個解決資料讀-寫問題的完美方案。當線程更新資料時鎖住資料。當沒有寫者線程擁有鎖的時候寫者線程可以獲得鎖。寫者鎖可以在沒有讀者線程或者寫者線程擁有鎖的時候獲得鎖。因此,ReaderWriterLock 就像是一段關鍵區段代碼, 它也支援一個timeout 值,而這方面在檢測死結時非常有用。
1.幾種同步方法的區別
lock和Monitor是.NET用一個特殊結構實現的,Monitor對象是完全託管的、完全可移植的,並且在作業系統資源要求方 面可能更為有效,同步速度較快,但不能跨進程同步。lock(Monitor.Enter和Monitor.Exit方法的封裝),主要作用是鎖定臨界區,使臨 界區代碼只能被獲得鎖的線程執行。Monitor.Wait和Monitor.Pulse用於線程同步,類似訊號操作,個人感覺使用比較複雜,容易造成死 鎖。
互斥體Mutex和事件對象EventWaitHandler屬於核心對象,利用核心對象進行線程同步,線程必須要在使用者模式和核心模 式間切換,所以一般效率很低,但利用互斥對象和事件對象這樣的核心對象,可以在多個進程中的各個線程間進行同步。
互斥體Mutex類似於一個接力棒,拿到接力棒的線程才可以開始跑,當然接力棒一次只屬於一個線程(Thread Affinity),如果這個線程不釋放接力棒(Mutex.ReleaseMutex),那麼沒辦法,其他所有需要接力棒啟動並執行線程都知道能等著看熱 鬧。
EventWaitHandle 類允許線程通過發訊號互相通訊。 通常,一個或多個線程在 EventWaitHandle 上阻止,直到一個未阻止的線程調用 Set 方法,以釋放一個或多個被阻止的線程。
2.什麼時候需要鎖定
首先要理解鎖定是解決競爭條件的,也就是多個線程同時訪問某個資源,造成意想不到的結果。比如,最簡單的情況是,一個計數器,兩個線程 同時加一,後果就是損失了一個計數,但相當頻繁的鎖定又可能帶來效能上的消耗,還有最可怕的情況死結。那麼什麼情況下我們需要使用鎖,什麼情況下不需要 呢?
1)只有共用資源才需要鎖定
只有可以被多線程訪問的共用資源才需要考慮鎖定,比如靜態變數,再比如某些緩衝中的值,而屬於線程內部的變數不需要鎖定。
2)多使用lock,少用Mutex
如果你一定要使用鎖定,請盡量不要使用核心模組的鎖定機制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用這樣的機制涉及到了系統在使用者模式和核心模式間的切換,效能差很多,但是他們的優點是可以跨進程同步線程,所以應該清 楚的瞭解到他們的不同和適用範圍。
3)瞭解你的程式是怎麼啟動並執行
實際上在web開發中大多數邏輯都是在單個線程中展開的,一個請求都會在一個單獨的線程中處理,其中的大部分變數都是屬於這個線程的,根本沒有必要考慮鎖 定,當然對於ASP.NET中的Application對象中的資料,我們就要考慮加鎖了。
4)把鎖定交給資料庫
數 據庫除了儲存資料之外,還有一個重要的用途就是同步,資料庫本身用了一套複雜的機制來保證資料的可靠和一致性,這就為我們節省了很多的精力。保證了資料來源 頭上的同步,我們多數的精力就可以集中在緩衝等其他一些資源的同步訪問上了。通常,只有涉及到多個線程修改資料庫中同一條記錄時,我們才考慮加鎖。
5)商務邏輯對事務和安全執行緒的要求
這 條是最根本的東西,開發完全安全執行緒的程式是件很費時費力的事情,在電子商務等涉及金融系統的案例中,許多邏輯都必須嚴格的安全執行緒,所以我們不得不犧牲 一些效能,和很多的開發時間來做這方面的工作。而一般的應用中,許多情況下雖然程式有競爭的危險,我們還是可以不使用鎖定,比如有的時候計數器少一多一, 對結果無傷大雅的情況下,我們就可以不用去管它。
3.InterLocked類
Interlocked 類提供了同步對多個線程共用的變數的訪問的方法。如果該變數位於共用記憶體中,則不同進程的線程就可以使用該機制。互鎖操作是原子的,即整個操作是不能由相 同變數上的另一個互鎖操作所中斷的單元。這在搶先多線程作業系統中是很重要的,在這樣的作業系統中,線程可以在從某個記憶體位址載入值之後但是在有機會更改 和儲存該值之前被掛起。
我們來看一個InterLock.Increment()的例子,該方法以原子的形式遞增指定變數並儲存結果,樣本如下:
class InterLockedTest
{
public static Int64 i = 0;
public static void Add()
{
for (int i = 0; i < 100000000; i++)
{
Interlocked.Increment(ref InterLockedTest.i);
//InterLockedTest.i = InterLockedTest.i + 1;
}
}
public static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(InterLockedTest.Add));
Thread t2 = new Thread(new ThreadStart(InterLockedTest.Add));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine(InterLockedTest.i.ToString());
Console.Read();
}
}
輸出結果200000000,如果InterLockedTest.Add()方法中用注釋掉的語句代替Interlocked.Increment() 方法,結果將不可預知,每次執行結果不同。InterLockedTest.Add()方法保證了加1操作的原子性,功能上相當於自動給加操作使用了 lock鎖。同時我們也注意到InterLockedTest.Add()用時比直接用+號加1要耗時的多,所以說加鎖資源損耗還是很明顯的。
另外InterLockedTest類還有幾個常用方法,具體用法可以參考MSDN上的介紹。
4.集合類的同步
.NET在一些集合類,比如Queue、ArrayList、HashTable和Stack,已經提供了一個供lock使用的對象SyncRoot。用 Reflector查看了SyncRoot屬性(Stack.SynchRoot略有不同)的源碼如下:
public virtual object SyncRoot
{
get
{
if (this._syncRoot == null)
{
//如果_syncRoot和null相等,將new object賦值給 _syncRoot
//Interlocked.CompareExchange方法保證多個線程在使用 syncRoot時是安全執行緒的
Interlocked.CompareExchange(ref this._syncRoot, new object(), null);
}
return this._syncRoot;
}
}
這裡要特別注意的是MSDN提到:從頭到尾對一個集合進行枚舉本質上並不是一個安全執行緒的過程。即使一個集合已進行同步,其他線程仍可以修改該集合,這將 導致枚舉數引發異常。若要在枚舉過程中保證安全執行緒,可以在整個枚舉過程中鎖定集合,或者捕捉由於其他線程進行的更改而引發的異常。應該使用下面的代碼:
Queue使用lock樣本
還有一點需要說明的是,集合類提供了一個是和同步相關的方法Synchronized,該 方法返回一個對應的集合類的wrapper類,該類是安全執行緒的,因為他的大部分方法都用lock關鍵字進行了同步處理。如HashTable的 Synchronized返回一個新的安全執行緒的HashTable執行個體,代碼如下:
//在多線程環境中只要我們用下面的 方式執行個體化HashTable就可以了
Hashtable ht = Hashtable.Synchronized(new Hashtable());
//以下代碼是.NET Framework Class Library實現,增加對 Synchronized的認識
[HostProtection(SecurityAction.LinkDemand, Synchronization=true)]
public static Hashtable Synchronized(Hashtable table)
{
if (table == null)
{
throw new ArgumentNullException("table");
}
return new SyncHashtable(table);
}
//SyncHashtable的幾個常用方法,我們可以看到內部實現都加了lock關鍵字 保證安全執行緒
public override void Add(object key, object value)
{
lock (this._table.SyncRoot)
{
this._table.Add(key, value);
}
}
public override void Clear()
{
lock (this._table.SyncRoot)
{
this._table.Clear();
}
}
public override void Remove(object key)
{
lock (this._table.SyncRoot)
{
this._table.Remove(key);
}
}
線程同步是一個非常複雜的話題,這裡只是根據公司的一個項目把相關的知識整理出來,作為工作的一種總結。這些同步方法的使用情境是怎樣的?究竟有哪些細微 的差別?還有待於進一步的學習和實踐。
C# 多線程並發鎖模式-總結