安全執行緒的一個很經常的需求是允許並發讀,但是不允許並發寫,例如對於檔案就是這樣的。
ReaderWriterLockSlim 在.net framework 3.5的時候就提供了,它是用來代替以前的”fat”版本的”ReaderWriterLock”
這兩個類,有兩種基本的鎖----一個讀鎖,一個寫鎖。
寫鎖是一個完全獨佔鎖定。
讀鎖可以和其他的讀鎖相容
因此當一個線程持有寫鎖的是很,所有的嘗試擷取讀鎖和寫鎖的線程全部阻塞,但是如果沒有一個線程持有寫鎖,那麼可以有一系列的線程並發的擷取讀鎖。
ReaderWriterLockSlim 定義了下面幾個方法來擷取和釋放 讀寫鎖。
- Public void EnterReadLock();
- Public void ExitReadLock();
- Public void EnterWriteLock();
- Public void ExitWriteLock();
和Monitor.TryEnter類似,ReaderWriterLockSlim 再對應的”EnterXXX”方法上也提供了相應的”Try”版本。ReaderWriterLock提供了AcquireXXX 和 ReleaseXXX 方法,當逾時發生了,ReaderWriterLock 拋出一個ApplicationException,而不是返回false。
static readonly ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
static List<int> _items = new List<int>();
static Random _rand = new Random();
public static void Main()
{
///三個讀線程
new Thread(Read).Start();
new Thread(Read).Start();
new Thread(Read).Start();
//兩個寫線程
new Thread(Write).Start("A");
new Thread(Write).Start("B");
}
static void Read()
{
while (true)
{
_rw.EnterReadLock();//擷取讀鎖
//類比讀的過程
foreach (int i in _items)
Thread.Sleep(100);
_rw.ExitReadLock();//釋放讀鎖
}
}
static void Write(object threadID)
{
while (true)
{
Console.WriteLine(_rw.CurrentReadCount + " concurrent readers");
int newNumber = GetRandomNum(100);
_rw.EnterWriteLock(); //擷取寫鎖
_items.Add(newNumber); //寫資料
_rw.ExitWriteLock(); //釋放寫鎖
Console.WriteLine("Thread " + threadID + " added " + newNumber);
Thread.Sleep(100);
}
}
//擷取隨機數
static int GetRandomNum(int max) { lock (_rand) return _rand.Next(max); }
再實際的發布版本中,最好使用try/finally 來確保即使異常拋出了,鎖也被正確的釋放了。
像CurrentReadCount 屬性,ReaderWriterLockSlim 提供了以下屬性用來監視鎖。
可更新鎖定:
再一個原子操作裡將讀鎖定擴大為寫鎖是很有用的,例如,假設你想要再一個list 裡面寫一些不存在的項的時候, 你可能會執行下面的一些步驟:
- 擷取一個讀鎖。
- 測試,如果要寫的東西在列表中,那麼釋放鎖,然後返回。
- 釋放讀鎖。
- 擷取一個寫鎖
- 添加項,寫東西,
- 釋放寫鎖。
問題是:在第三步和第四步之間,可能有另一個線程修改了列表。
ReaderWriterLockSlim 通過一個叫做可更新鎖定( upgradeable lock),來解決這個問題。
一個可更新鎖定除了它可以在一個原子操作中變成寫鎖外很像一個讀鎖,你可以這樣使用它:
- 調用EnterUpgradeableReadLock 擷取可更新鎖定。
- 執行一些讀操作,例如判斷要寫的東西在不在List中。
- 調用EnterWriteLock , 這個方法會將可更新鎖定 升級為 寫鎖。
- 執行寫操作,
- 調用ExitWriteLock 方法,這個方法將寫鎖轉換回可更新鎖定。
- 繼續執行一些讀操作,或什麼都不做。
- 調用ExitUpgradeableReadLock 釋放可更新鎖定。
從調用者的角度來看,它很像一個嵌套/遞迴鎖,從功能上講,在第三步,
ReaderWriterLockSlim 在一個原子操作裡面釋放讀鎖,然後擷取寫鎖。
可更新鎖定和讀鎖的重要區別是:儘管可更新鎖定可以和讀鎖共存,但是一次只能有一個可更新鎖定被擷取。這樣的主要目的是防止死結。
這樣我們可以修改Write方法,讓它可以添加一些不在列表中的Item。
static void Write(object threadID)
{
while (true)
{
Console.WriteLine(_rw.CurrentReadCount + " concurrent readers");
int newNumber = GetRandomNum(100);
_rw.EnterUpgradeableReadLock(); //擷取可更新鎖定
if (!_items.Contains(newNumber)) //如果要寫的東西不在列表中
{
_rw.EnterWriteLock(); //可更新鎖定變成寫鎖
_items.Add(newNumber); //寫東西
_rw.ExitWriteLock(); //重新變回可更新鎖定
Console.WriteLine("Thread " + threadID + " added " + newNumber); //讀資料
}
_rw.ExitUpgradeableReadLock(); //退出可更新鎖定
Thread.Sleep(100);
}
}
從上面的例子可以看到C#提供的讀寫鎖功能強大,使用方便,
所以在自己編寫讀寫鎖的時候,要考慮下是否需要支援可更新鎖定,是否有必要自己寫一個讀寫鎖.
參考資料:
http://www.albahari.com/threading/
CLR Via C# 3.0