profile for Macon_Cao at Stack Overflow, Q&A for professional and enthusiast programmers
随笔 - 130  文章 - 0  评论 - 195  阅读 - 13万

看看这个Lock可不可靠

 

由于业务的需要,设计了一个Lock。

这个Lock的设计要求如下:

  1. 数据被多线程访问
  2. 对数据的访问分为读和写
  3. 当任一线程读数据时,其它线程不能写数据
  4. 当任一线程写数据时,其它线程不能写数据,其它线程不能读数据
  5. 由于读数据的频率远远高于写数据的频率,所以读数据线程的优先级更高
  6. 不允许死锁的情况发生

这里实际上要求的是读和写的互斥,写和写之间的互斥,但读与读之间并不互斥。显然,不能用一个简单的锁来搞定。例如下面的代码

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);
                }
            }
 
        }
posted on   涵树  阅读(2673)  评论(35编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述
< 2010年1月 >
27 28 29 30 31 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 1 2 3 4 5 6

点击右上角即可分享
微信分享提示