• 看看这个Lock可不可靠 CQ


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

    这个Lock的设计要求如下:

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

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

    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);
                    }
                }
    
            }
  • 相关阅读:
    配置postgres9.3间的fdw——实现不同postgres数据库间的互访问
    linux安装配置postgres及使用dblink
    一次“峰回路转”的troubleshooting经历
    10分钟内把永远跑不完的存储过程变为2秒跑完
    C++ friend关键字
    每天学点Linux命令之 vi 命令
    Shell
    九大排序算法及其实现- 插入.冒泡.选择.归并.快速.堆排序.计数.基数.桶排序.堆排序
    到位
    【LeetCode】-- 260. Single Number III
  • 原文地址:https://www.cnblogs.com/czy/p/1659552.html
Copyright © 2020-2023  润新知