设为首页 加入收藏

TOP

.NET 同步与异步之锁(Lock、Monitor)(七)(一)
2019-09-03 01:14:17 】 浏览:36
Tags:.NET 同步 异步 Lock Monitor

本随笔续接:.NET同步与异步之相关背景知识(六)

 

在上一篇随笔中已经提到、解决竞争条件的典型方式就是加锁 ,那本篇随笔就重点来说一说.NET提供的最常用的锁 lock关键字 和 Monitor。

一、lock关键字Demo

        public object thisLock = new object();
        private long index;

        public void AddIndex()
        {
            lock (this.thisLock)
            {
                this.index++;

                if (this.index > long.MaxValue / 2)
                {
                    this.index = 0;
                }
         // 和 index 无关的大量操作 } }
public long GetIndex() { return this.index; }

 

这一组demo,代码简洁,逻辑简单,一个 AddIndex 方法 保证字段 index 在 0到100之间,另外一个GetIndex方法用来获取字段index的值。

但是,这一组Demo却有不少问题,甚至可以说是错误,下面我将一一进行说明:

1、忘记同步——即读写操作都需要加锁

  GetIndex方法, 由于该方法没有加锁,所以通过该方法在任何时刻都可以访问字段index的值,也就是说会恰好在某个时间点获取到 101 这个值,这一点是和初衷相违背的。

 

2、读写撕裂

  如果说读写撕裂这个问题,这个demo可能不是很直观,但是Long类型确实存在读写撕裂。比如下面的例子:

        /// <summary>
        /// 测试原子性
        /// </summary>
        public void TestAtomicity()
        {
            long test = 0;

            long breakFlag = 0;
            int index = 0;
            Task.Run(() =>
            {
                base.PrintInfo("开始循环   写数据");
                while (true)
                {
                    test = (index % 2 == 0) ? 0x0 : 0x1234567890abcdef;

                    index++;

                    if (Interlocked.Read(ref breakFlag) > 0)
                    {
                        break;
                    }
                }

                base.PrintInfo("退出循环   写数据");
            });

            Task.Run(() =>
            {
                base.PrintInfo("开始循环   读数据");
                while (true)
                {
                    long temp = test;

                    if (temp != 0 && temp != 0x1234567890abcdef)
                    {
                        Interlocked.Increment(ref breakFlag);
                        base.PrintInfo($"读写撕裂:   { Convert.ToString(temp, 16)}");
                        break;
                    }
                }

                base.PrintInfo("退出循环   读数据");
            });
        }
测试原子性操作

64位的数据结构 在32位的系统上(当然和CPU也有关系)是需要两个命令来实现读写操作的,也就是说、如果恰好在两个写命令中间发生了读取操作,就有可能读取到不完成的数据。故而要警惕读写撕裂。

 

3、粒度错误

  AddIndex 方法中,和 index 无关的大量操作 ,放在锁中是没有必要的,虽然没必要但是也不是错的,只能说这个锁的粒度过大,造成了没必要的并发上的性能影响。

下面举例一个错误的锁粒度:

        public class BankAccount
        {
            private long id;
            private decimal m_balance = 0.0M;

            private object m_balanceLock = new object();

            public void Deposit(decimal delta)
            {
                lock (m_balanceLock)
                {
                    m_balance += delta;
                }
            }

            public void Withdraw(decimal delta)
            {
                lock (m_balanceLock)
                {
                    if (m_balance < delta)
                        throw new Exception("Insufficient funds");
                    m_balance -= delta;
                }
            }

            public static void ErrorTransfer(BankAccount a, BankAccount b, decimal delta)
            {
                a.Withdraw(delta);
                b.Deposit(delta);
            }


            public static void Transfer(BankAccount a, BankAccount b, decimal delta)
            {
                lock (a.m_balanceLock)
                {
                    lock (b.m_balanceLock)
                    {
                        a.Withdraw(delta);
                        b.Deposit(delta);
                    }
                }
            }

            public static void RightTransfer(BankAccount a, BankAccount b, decimal delta)
            {
                if (a.id < b.id)
                {
                    Monitor.Enter(a.m_balanceLock); // A first
                    Monitor.Enter(b.m_balanceLock); // ...and then B
                }
                else
                {
                    Monitor.Enter(b.m_balanceLock); // B first
                    Monitor.Enter(a.m_balanceLock); // ...and then A 
                }

                try
                {
                    a.Withdraw(delta);
                    b.Deposit(delta);
                }
                finally
                {
                    Monitor.Exit(a.m_balanceLock);
                    Monitor.Exit(b.m_balanceLock);
                }
            }

        }
错误的锁粒度

在 ErrorTransfer 方法中,在转账的两个方法中间的时间点上,转账金额属于无主状态,这时锁的粒度就过小了 。

在 Transfer 方法中,虽然粒度正确了,但是此时容易死锁。而比较恰当的方式可以是:RightTransfer 。

 

4、不合理的lock方式

锁定非私有类型的对象是一种危险的行为,因为非私有类型被暴露给外界、外界也可以对被暴露的对象进行加锁,这种情况下很容造成死锁 或者 错误的锁粒度。

较为合理的方式是 将 thislock 改为 private .

由上述进行类推:

1、lock(this):如果当前类型为外界可访问的也会有类似问题。

2、lock(typeof(T)): 因为Type对象,是整个进程域中是唯一的。所以,如果T为外界可访问的类型也会有类似问题。

3、loc

首页 上一页 1 2 下一页 尾页 1/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇EF 下的code fist 模式编程 下一篇2017 年不可错过的开发工具 Top 50

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目