由于业务的需要,设计了一个Lock。
这个Lock的设计要求如下:
- 数据被多线程访问
- 对数据的访问分为读和写
- 当任一线程读数据时,其它线程不能写数据
- 当任一线程写数据时,其它线程不能写数据,其它线程不能读数据
- 由于读数据的频率远远高于写数据的频率,所以读数据线程的优先级更高
- 不允许死锁的情况发生
这里实际上要求的是读和写的互斥,写和写之间的互斥,但读与读之间并不互斥。显然,不能用一个简单的锁来搞定。例如下面的代码
class DataService<T> { List<T> mCache = new List<T>(); private object mLockObject = new object(); public List<T> Search(Predicate<T> predicate) { lock (mLockObject) { return mCache.FindAll(predicate); } } public void Upate(int key) { lock (mLockObject) { //somemethod to update mCache with key } } }
这段代码确实是实现了读写互斥,写与写的互斥,但是读与读之间也发生了互斥。由于读的频率很高,所以读与读之间的互斥会直接影响到系统的性能。
为此,我设计了2个Lock
private object mWriteLock = new object(); private object mReadLock = new object();
写与写的互斥
mWriteLock用于独占写入,但它仅仅控制的是写操作,对读操作没有影响。
public void LockToWrite(Action action) { lock (mWriteLock) { action(); } }
读与读之间不互斥
mReadLock用于读操作,但它并不直接锁定读操作本身,而是锁定读操作的计数。所以,和mReadLock一同工作的还有一个数据
private int mReadSeed = 0;
mReadLock实际上是用于锁定mReadSeed的读写。
private void IncrementReadSeeds() { lock (mReadLock) mReadSeed++; } private void DecrementReadSeeds() { lock (mReadLock) mReadSeed--; }
当读操作发生时,调用以上方法
public T LockToRead<T>(Func<T> action) { try { IncrementReadSeeds(); return action(); } finally { DecrementReadSeeds(); } }
在读操作中,由于只是锁定的mReadSeeds的运算,而不是锁定的action运算,所以即便是action的运行时间会很长,也不会阻止其它线程的读操作。
其实这里暴露了一个问题,因为锁定的只是所谓的读操作,而锁定的数据却根本没有提及,即没有办法从代码上保证,action就不会对真正要保护的资源进行写操作。还好,这个类只会作为DataService类中的一个子类存在,这样就可以通过约定来解决上面的顾虑(目前水平有限,也只能出此下策)
读与写之间的互斥
读与写之间的互斥,需要将两个锁关联起来。这段代码很关键。
public void WaitToWrite() { while (true) { lock (mReadLock) { if (mReadSeed == 0) { mCanRead = false; break; } } Thread.Sleep(1); } }
在WaitToWrite方法中,使用了mReadLock,当mReadSeed==0是,将mCanRead标记为false。
在LockToRead方法中,我们可以看到,只有当所有的读操作都完成后,mReadSeed才可能为0。而当所有的读操作完成后,WaitToWrite方法将mCanRead置为false。而这个操作受到了mReadLock的保护。
于是读的代码要稍作调整
public T LockToRead<T>(Func<T> action) { try { while (true) { WaitToRead(); IncrementReadSeeds(); if (!mCanRead)//因为IncrementReadSeeds和WaitToWrite都使用了mReadLock,所以我认为这里读取mCanRead是安全的,这是这个算法的关键,不知道大家是否这么看? { DecrementReadSeeds(); continue; } else break; } return action(); } finally { DecrementReadSeeds(); } }
在上面的代码中调用了WaitToRead()方法,它实际上是用来判断当前的读操作是否被允许的第一道关卡。
WaitToWrite同样被置于mWriteLock的保护之下,那同样意味着mCanRead=false操作受到了mWriteLock的保护。写的代码调整为
public T LockToWrite<T>(Func<T> action) { lock (mWriteLock) { WaitToWrite();//等待读操作完成 try { return action(); } finally { mCanRead = true;//将允许读的操作置为true } } }
是否会出现读写互锁
我认为问题的关键在于mReadSeeds==0的状态与mCanRead的状态处理上。
mReadSeeds的处理依赖于mReadLock,mCanRead置为false的处理也依赖于mReadLock的保护,所以IncrementReadSeeds();if(!mCanRead)也受到了mReadLock的保护。
而WatiToWrite(那么将mCanRead置为false的方法)受到了mWriteLock的保护,所以mCanRead对于写的操作是安全的。
因为mReadSeeds==0并不受到写操作的影响,所以不会出现读写互锁的情况。
以下是LockClass的全部代码,还望大家多多指教。
另外,感谢msolap的建议。我会尝试用他的建议来优化代码。
class LockClass { private bool mCanRead = false; private object mWriteLock = new object(); private object mReadLock = new object(); private int mReadSeed = 0; private void WaitToWrite() { while (true) { lock (mReadLock) { if (mReadSeed == 0) { mCanRead = false; break; } } Thread.Sleep(1); } } public void LockToWrite(Action action) { lock (mWriteLock) { WaitToWrite(); try { action(); } finally { mCanRead = true; } } } private void IncrementReadSeeds() { lock (mReadLock) mReadSeed++; } private void DecrementReadSeeds() { lock (mReadLock) mReadSeed--; } public T LockToRead<T>(Func<T> action) { try { while (true) { WaitToRead(); IncrementReadSeeds(); if (!mCanRead) { DecrementReadSeeds(); continue; } else break; } return action(); } finally { DecrementReadSeeds(); } } private void WaitToRead() { while (!mCanRead) { Thread.Sleep(1); } } }