深入多线程之:Reader与Write Locks(读写锁)的使用详解
线程安全的一个很经常的需求是允许并发读,但是不允许并发写,例如对于文件就是这样的。
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#提供的读写锁功能强大,使用方便,
所以在自己编写读写锁的时候,要考虑下是否需要支持可更新锁,是否有必要自己写一个读写锁.