如何處理幾十萬條併發資料
資源洩漏。在 .NET 2.0 之前的版本中, ReaderWriterLock 類會造成核心物件洩露。這些物件只有在程序終止後才能再次回收。幸運的是,.NET 2.0 修正了這個 Bug 。
此外,ReaderWriterLock 還有個令人擔心的危險的非原子性操作。它就是 UpgradeToWriteLock 方法。這個方法實際上在更新到寫鎖前先釋放了讀鎖。這就讓其他執行緒有機會在此期間乘虛而入,從而獲得讀寫鎖且改變狀態。如果先更新到寫鎖,然後釋放讀鎖。 假如兩個執行緒同時更新將會導致另外一個執行緒死鎖。
所以 Microsoft 決定構建一個新類來一次性解決上述所有問題,這就是 ReaderWriterLockSlim 類。本來可以在原有的 ReaderWriterLock 類上修正錯誤,但是考慮到相容性和已存在的 API ,Microsoft 放棄了這種做法。當然也可以標記 ReaderWriterLock 類為 Obsolete,但是由於某些原因,這個類還有存在的必要。
ReaderWriterLockSlim 類
新的 ReaderWriterLockSlim 類支援三種鎖定模式:Read,Write,UpgradeableRead。這三種模式對應的方法分別是 EnterReadLock,EnterWriteLock,EnterUpgradeableReadLock 。再就是與此對應的 TryEnterReadLock,TryEnterWriteLock,TryEnterUpgradeableReadLock,ExitReadLock,ExitWriteLock,ExitUpgradeableReadLock。 Read 和 Writer 鎖定模式比較簡單易懂:Read 模式是典型的共享鎖定模式,任意數量的執行緒都可以在該模式下同時獲得鎖;Writer 模式則是互斥模式,在該模式下只允許一個執行緒進入該鎖。UpgradeableRead 鎖定模式可能對於大多數人來說比較新鮮,但是在資料庫領域卻眾所周知。
這個新的讀寫鎖類效能跟 Monitor 類大致相當,大概在 Monitor 類的 2 倍之內。而且新鎖優先讓寫執行緒獲得鎖,因為寫操作的頻率遠小於讀操作。通常這會導致更好的可伸縮性。起初,ReaderWriterLockSlim 類在設計時考慮到相當多的情況。比如在早期 CTP 的程式碼還提供了PrefersReaders, PrefersWritersAndUpgrades 和 Fifo 等競爭策略。但是這些策略雖然新增起來非常簡單,但是會導致情況非常的複雜。所以 Microsoft 最後決定提供一個能夠在大多數情況下良好工作的簡單模型。
ReaderWriterLockSlim 的更新鎖
現在讓我們更加深入的討論一下更新模型。UpgradeableRead 鎖定模式允許安全的從 Read 或 Write 模式下更新。還記得先前 ReaderWriterLock 的更新是非原子性,危險的操作嗎(尤其是大多數人根本沒有意識到這點)?現在提供的新讀寫鎖既不會破壞原子性,也不會導致死鎖。新鎖一次只允許一個執行緒處 在 UpgradeableRead 模式下。
一旦該讀寫鎖處在 UpgradeableRead 模式下,執行緒就能讀取某些狀態值來決定是否降級到 Read 模式或升級到 Write 模式。注意應當儘可能快的作出這個決定:持有 UpgradeableRead 鎖會強制任何新的讀請求等待,儘管已存在的讀取操作仍然活躍。遺憾的是,CLR 團隊移除了 DowngradeToRead 和 UpgradeToWrite 兩個方法。如果要降級到讀鎖,只要簡單的在 ExitUpgradeableReadLock 方法後緊跟著呼叫 EnterReadLock 方法即可:這可以讓其他的 Read 和 UpgradeableRead 獲得完成先前應當持有卻被 UpgradeableRead 鎖持有的操作。如果要升級到寫鎖,只要簡單呼叫 EnterWriteLock 方法即可:這可能要等待,直到不再有任何執行緒在 Read 模式下持有鎖。不像降級到讀鎖,必須呼叫 ExitUpgradeableReadLock。在 Write 模式下不必非得呼叫 ExitUpgradeableReadLock。但是為了形式統一,最好還是呼叫它。比如下面的程式碼:
using System;
ReaderWriterLockSlim 的遞迴策略
using System.Linq;
using System.Threading;
namespace Lucifer.CSharp.Sample
{
class Program
{
private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
void Sample()
{
bool isUpdated = true;
rwLock.EnterUpgradeableReadLock();
try
{
if (/* … 讀取狀態值來決定是否更新 … */)
{
rwLock.EnterWriteLock();
try
{
//… 寫入狀態值 …
}
finally
{
rwLock.ExitWriteLock();
}
}
else
{
rwLock.EnterReadLock();
rwLock.ExitUpgradeableReadLock();
isUpdated = false;
try
{
//… 讀取狀態值 …
}
finally
{
rwLock.ExitReadLock();
}
}
}
finally
{
if (isUpdated)
rwLock.ExitUpgradeableReadLock();
}
}
}
}
新的讀寫鎖還有一個有意思的特性就是它的遞迴策略。預設情況下,除已提及的降級到讀鎖和升級到寫鎖之外,所有的遞迴請求都不允許。這意味著你不能連續兩 次呼叫 EnterReadLock,其他模式下也類似。如果你這麼做,CLR 將會丟擲 LockRecursionException 異常。當然,你可以使用 LockRecursionPolicy.SupportsRecursion 的建構函式引數讓該讀寫鎖支援遞迴鎖定。但不建議對新的開發使用遞迴,因為遞迴會帶來不必要的複雜情況,從而使你的程式碼更容易出現死鎖現象。
有一種特殊的情況永遠也不被允許,無論你採取什麼樣的遞迴策略。這就是當執行緒持有讀鎖時請求寫鎖。Microsoft 曾經考慮提供這樣的支援,但是這種情況太容易導致死鎖。所以 Microsoft 最終放棄了這個方案。
此外,這個新的讀寫鎖還提供了很多對應的屬性來確定執行緒是否在指定模型下持有該鎖。比如 IsReadLockHeld, IsWriteLockHeld 和 IsUpgradeableReadLockHeld 。你也可以通過 WaitingReadCount,WaitingWriteCount 和 WaitingUpgradeCount 等屬性來檢視有多少執行緒正在等待持有特定模式下的鎖。CurrentReadCount 屬性則告知目前有多少併發讀執行緒。RecursiveReadCount, RecursiveWriteCount 和 RecursiveUpgradeCount 則告知目前執行緒進入特定模式鎖定狀態下的次數。
小結
這篇文章分析了 .NET 中提供的兩個讀寫鎖類。然而 .NET 3.5 提供的新讀寫鎖 ReaderWriterLockSlim 類消除了 ReaderWriterLock 類存在的主要問題。與 ReaderWriterLock 相比,效能有了極大提高。更新具有原子性,也可以極大避免死鎖。更有清晰的遞迴策略。在任何情況下,我們都應該使用 ReaderWriterLockSlim 來代替 ReaderWriterLock 類。
Update 於 2008-12-07 0:06
Windows Vista 及其以後的版本新增了一個 SRWLock 原語。它以 Windows 核心事件機制為基礎而構建。它的設計比較有意思。
SRW 鎖不支援遞迴。Windows Kernel 團隊認為支援遞迴會造成額外系統開銷,原因是為了維持準確性需進行逐執行緒的計數。SRW 鎖也不支援從共享訪問升級到獨佔訪問,同時也不支援從獨佔訪問降級到共享訪問。支援升級能力可能會造成難以接受的複雜性和額外系統開銷,這種開銷甚至會影 響鎖內共享和獨佔獲得程式碼的常見情況。它還要求定義關於如何選擇等待中的讀取器、等待中的寫入器和等待升級的讀取器的策略,這又將與無偏向的基本設計目標 相抵觸。我對其進行了 .NET 封裝。程式碼如下:
using System;
此外,在其他平臺也有一些有意思的讀寫鎖。比如 Linux 核心中的讀寫鎖和 Java 中的讀寫鎖。感興趣的同學可以自己研究一番。
using System.Threading;
using System.Runtime.InteropServices;
namespace Lucifer.Threading.Lock
{
/// <summary>
/// Windows NT 6.0 才支援的讀寫鎖。
/// </summary>
/// <remarks>請注意,這個類只能在 NT 6.0 及以後的版本中才能使用。</remarks>
public sealed class SRWLock
{
private IntPtr rwLock;
/// <summary>
/// 該鎖不支援遞迴。
/// </summary>
public SRWLock()
{
InitializeSRWLock(out rwLock);
}
/// <summary>
/// 獲得讀鎖。
/// </summary>
public void EnterReadLock()
{
AcquireSRWLockShared(ref rwLock);
}
/// <summary>
/// 獲得寫鎖。
/// </summary>
public void EnterWriteLock()
{
AcquireSRWLockExclusive(ref rwLock);
}
/// <summary>
/// 釋放讀鎖。
/// </summary>
public void ExitReadLock()
{
ReleaseSRWLockShared(ref rwLock);
}
/// <summary>
/// 釋放寫鎖。
/// </summary>
public void ExitWriteLock()
{
ReleaseSRWLockExclusive(ref rwLock);
}
[DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
private static extern void InitializeSRWLock(out IntPtr rwLock);
[DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
private static extern void AcquireSRWLockExclusive(ref IntPtr rwLock);
[DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
private static extern void AcquireSRWLockShared(ref IntPtr rwLock);
[DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
private static extern void ReleaseSRWLockExclusive(ref IntPtr rwLock);
[DllImport("Kernel32", CallingConvention = CallingConvention.Winapi, ExactSpelling = true)]
private static extern void ReleaseSRWLockShared(ref IntPtr rwLock);
}
}
讀寫鎖有個很常用的場景就是在快取設計中。因為快取中經常有些很穩定,不太長更新的內容。MSDN 的程式碼示例就很經典,我原版拷貝一下,呵呵。程式碼示例如下:
using System;
using System.Threading;
namespace Lucifer.CSharp.Sample
{
public class SynchronizedCache
{
private ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
private Dictionary<int, string> innerCache = new Dictionary<int, string>();
public string Read(int key)
{
cacheLock.EnterReadLock();
try
{
return innerCache[key];
}
finally
{
cacheLock.ExitReadLock();
}
}
public void Add(int key, string value)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public bool AddWithTimeout(int key, string value, int timeout)
{
if (cacheLock.TryEnterWriteLock(timeout))
{
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return true;
}
else
{
return false;
}
}
public AddOrUpdateStatus AddOrUpdate(int key, string value)
{
cacheLock.EnterUpgradeableReadLock();
try
{
string result = null;
if (innerCache.TryGetValue(key, out result))
{
if (result == value)
{
return AddOrUpdateStatus.Unchanged;
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache[key] = value;
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Updated;
}
}
else
{
cacheLock.EnterWriteLock();
try
{
innerCache.Add(key, value);
}
finally
{
cacheLock.ExitWriteLock();
}
return AddOrUpdateStatus.Added;
}
}
finally
{
cacheLock.ExitUpgradeableReadLock();
}
}
public void Delete(int key)
{
cacheLock.EnterWriteLock();
try
{
innerCache.Remove(key);
}
finally
{
cacheLock.ExitWriteLock();
}
}
public enum AddOrUpdateStatus
{
Added,
Updated,
Unchanged
};
}
}
再次 Update 於 2008-12-07 0:47
如果應用場景要求效能十分苛刻,可以考慮採用 lock-free 方案。但是 lock-free 有著固有缺陷:極難編碼,極難證明其正確性。讀寫鎖方案的應用範圍更加廣泛一些。