并发数据(锁)ReaderWriterLockSlim
ReaderWriterLockSlim有两种基本的Lock方法:一个独占的Wirte Lock ,和一个与其他Read lock相容的读锁定。
所以,当一个线程拥有一个Write Lock的时候,会阻塞所有其他线程获得读写锁。但是当没有线程获得WriteLock时,可以有多个线程同时获得ReadLock,进行读操作。
ReaderWriterLockSlim提供了下面四个方法来得到和释放读写锁:
public void EnterReadLock();
public void ExitReadLock();
public void EnterWriteLock();
public void ExitWriteLock();
另外对于所有的EnterXXX方法,还有”Try”版本的方法,它们接收timeOut参数,就像Monitor.TryEnter一样(在资源争用严重的时候超时发生相当容易)。另外ReaderWriterLock提供了其他类似的AcquireXXX 和 ReleaseXXX方法,它们超时退出的时候抛出异常而不是返回false。
下面的程序展示了ReaderWriterLockSlim——三个线程循环地枚举一个List,同时另外两个线程每一秒钟添加一个随机数到List中。一个read lock保护List的读取线程,同时一个write lock保护写线程。
class SlimDemo
{
static ReaderWriterLockSlim rw = new ReaderWriterLockSlim();
static List<int> items = new List<int>();
static Random rand = new Random();
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 (10);
rw.ExitReadLock();
}
}
static void Write (object threadID)
{
while (true)
{
int newNumber = GetRandNum (100);
rw.EnterWriteLock();
items.Add (newNumber);
rw.ExitWriteLock();
Console.WriteLine ("Thread " + threadID + " added " + newNumber);
Thread.Sleep (100);
}
}
static int GetRandNum (int max) { lock (rand) return rand.Next (max); }
}
//在实际的代码中添加try/finally,保证异常情况写lock也会被释放。
结果为:
Thread B added 61
Thread A added 83
Thread B added 55
Thread A added 33
...
ReaderWriterLockSlim比简单的Lock允许更大的并发读能力。我们能够添加一行代码到Write方法,在While循环的开始:
Console.WriteLine (rw.CurrentReadCount + " concurrent readers");
基本上总是会返回“3 concurrent readers”(读方法花费了更多的时间在Foreach循环),ReaderWriterLockSlim还提供了许多与CurrentReadCount属性类似的属性来监视lock的情况:
public bool IsReadLockHeld { get; }
public bool IsUpgradeableReadLockHeld { get; }
public bool IsWriteLockHeld { get; }
public int WaitingReadCount { get; }
public int WaitingUpgradeCount { get; }
public int WaitingWriteCount { get; }
public int RecursiveReadCount { get; }
public int RecursiveUpgradeCount { get; }
public int RecursiveWriteCount { get; }
有时候,在一个原子操作里面交换读写锁是非常有用的,比如,当某个item不在list中的时候,添加此item进去。最好的情况是,最小化写如锁的时间,例如像下面这样处理:
1 获得一个读取锁
2 测试list是否包含item,如果是,则返回
3 释放读取锁
4 获得一个写入锁
5 写入item到list中,释放写入锁。
但 是在步骤3、4之间,当另外一个线程可能偷偷修改List(比如说添加同样一个Item),ReaderWriterLockSlim通过提供第三种锁来 解决这个问题,这就是upgradeable lock。一个可升级锁和read lock 类似,只是它能够通过一个原子操作,被提升为write lock。使用方法如下:
-
- 调用 EnterUpgradeableReadLock
- 读操作(e.g. test if item already present in list)
- 调用 EnterWriteLock (this converts the upgradeable lock to a write lock)
- 写操作(e.g. add item to list)
- 调用ExitWriteLock (this converts the write lock back to an upgradeable lock)
- 其他读取的过程
- 调用ExitUpgradeableReadLock
从调用者的角度,这非常想递归(嵌套)锁。实际上第三步的时候,通过一个原子操作,释放了read lock 并获得了一个新的write lock.
upgradeable locks 和read locks之间另外还有一个重要的区别,尽管一个upgradeable locks 能够和任意多个read locks共存,但是一个时刻,只能有一个upgradeable lock自己被使用。这防止了死锁。这和SQL Server的Update lock类似
我们可以改变前面例子的Write方法来展示upgradeable lock:
while (true)
{
int newNumber = GetRandNum (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);
}
ReaderWriterLock 没有提供upgradeable locks的功能。
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的实例放到try{}块中,以减少性能损耗。
例如:
{
private static ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
private object Get()
{
object obj = null;
if (rwLock.TryEnterReadLock(100))
{
try
{
//写操作
obj = new object();
return obj;
}
finally
{
rwLock.ExitReadLock();
}
}
return null;
}
private void Add()
{
if (rwLock.TryEnterWriteLock(100))
{
try
{
//写操作
}
finally
{
rwLock.ExitWriteLock();
}
}
}
public void Update()
{
if (rwLock.TryEnterUpgradeableReadLock(100))
{
try
{
//读操作
rwLock.EnterWriteLock();
try
{
//写操作
}
finally
{
rwLock.ExitWriteLock();
}
}
finally
{
rwLock.ExitUpgradeableReadLock();
}
}
}
}