看看这个Lock可不可靠
由于业务的需要,设计了一个Lock。
这个Lock的设计要求如下:
- 数据被多线程访问
- 对数据的访问分为读和写
- 当任一线程读数据时,其它线程不能写数据
- 当任一线程写数据时,其它线程不能写数据,其它线程不能读数据
- 由于读数据的频率远远高于写数据的频率,所以读数据线程的优先级更高
- 不允许死锁的情况发生
这里实际上要求的是读和写的互斥,写和写之间的互斥,但读与读之间并不互斥。显然,不能用一个简单的锁来搞定。例如下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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
1 2 | private object mWriteLock = new object (); private object mReadLock = new object (); |
写与写的互斥
mWriteLock用于独占写入,但它仅仅控制的是写操作,对读操作没有影响。
1 2 3 4 5 6 7 | public void LockToWrite(Action action) { lock (mWriteLock) { action(); } } |
读与读之间不互斥
mReadLock用于读操作,但它并不直接锁定读操作本身,而是锁定读操作的计数。所以,和mReadLock一同工作的还有一个数据
1 | private int mReadSeed = 0; |
mReadLock实际上是用于锁定mReadSeed的读写。
1 2 3 4 5 6 7 8 9 10 | private void IncrementReadSeeds() { lock (mReadLock) mReadSeed++; } private void DecrementReadSeeds() { lock (mReadLock) mReadSeed--; } |
当读操作发生时,调用以上方法
1 2 3 4 5 6 7 8 9 10 11 12 13 | public T LockToRead<T>(Func<T> action) { try { IncrementReadSeeds(); return action(); } finally { DecrementReadSeeds(); } } |
在读操作中,由于只是锁定的mReadSeeds的运算,而不是锁定的action运算,所以即便是action的运行时间会很长,也不会阻止其它线程的读操作。
其实这里暴露了一个问题,因为锁定的只是所谓的读操作,而锁定的数据却根本没有提及,即没有办法从代码上保证,action就不会对真正要保护的资源进行写操作。还好,这个类只会作为DataService类中的一个子类存在,这样就可以通过约定来解决上面的顾虑(目前水平有限,也只能出此下策)
读与写之间的互斥
读与写之间的互斥,需要将两个锁关联起来。这段代码很关键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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的保护。
于是读的代码要稍作调整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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的保护。写的代码调整为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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的建议。我会尝试用他的建议来优化代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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); } } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述