liyonghui

导航

并发数据(锁)ReaderWriterLockSlim


通常来讲,一个类型的实例对于并行的读操作是线程安全的,但是并行地更新操作则不是(并行地读与更新也不是)。 这对于资源(比如一个文件)也是一样的。使用一个简单的独占锁来锁定所有可能的访问能够解决实例的线程安全为问题,但是当有很多的读操作而只是偶然的更新 操作的时候,这就很不合理的限制了并发。一个例子就是这在一个业务程序服务器中,为了快速查找把数据缓存到静态字段中。在这样的情况 下,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。使用方法如下:



  1.  

    1. 调用 EnterUpgradeableReadLock

    2. 读操作(e.g. test if item already present in list)

    3. 调用 EnterWriteLock (this converts the upgradeable lock to a write lock)

    4. 写操作(e.g. add item to list)

    5. 调用ExitWriteLock (this converts the write lock back to an upgradeable lock)

    6. 其他读取的过程

    7. 调用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{}块中,以减少性能损耗。

例如:

public class ReaderWriterLockSlimSample
{
    
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();
            }
        }

    }
}

 

 

 

 

 

 

 

 

posted on 2012-11-23 12:51  李永辉  阅读(1302)  评论(0编辑  收藏  举报