异步多线程 3 多线程安全

多线程去访问同一个集合,一般没问题,线程安全问题一般是出在同时修改一个对象的时候。

线程安全问题:一段代码,单线程执行和多线程执行,结果不一致。

例如这个方法,很简单,循环开启task往一个list里add,等3秒钟执行完后打印出来List的长度,如果是单线程,肯定是10000,现在看看多线程的结果。

        private static void AsyncSafeMethod()
        {
            List<int> listInt = new List<int>();
            for (int i = 0; i < 10000; i++)
            {
                Task.Run(() => {
                    listInt.Add(i);
                });
            }
            Thread.Sleep(3000);
            Console.WriteLine(listInt.Count);
        }

 

 

 而且基本上每次执行的结果都不一样,这说明,出现了多线程安全的问题,因为当多个线程会出现同时对list的同一个下标进行add的操作,导致值的覆盖,也就是同时往内存的同一个地址进行了多次赋值,导致先赋值进去的丢失。

 

 

最常用的解决方案是 Lock

Lock是Monitor的语法糖,它其实是锁住了一个内存的引用地址,所以Lock的对象肯定不能是值类型,因为值类型就保存在栈中,根本就没有向堆引用,那怎么锁,另外null也不能锁。

那么当多线程执行到Lock这里时,第一个到达的线程会占据这个lock_obj的引用,也就是加了个状态,当别的线程企图访问这个代码块前,需要先看看这个lock_obj的状态,发现被占据了,就会等待至占据的线程执行完代码块并重置这个状态。至于会不会多个线程同时占据引用,目前是不会,因为这是原子级别的操作,想一想事务的原子性的概念,不可分割,大概是这个意思。

声明一个object对象,用lock锁住Add这一段

        /// <summary>
        /// 锁对象,官方标准推荐写法。
        /// </summary>
        private static readonly object lock_Obj = new object();
        private static void AsyncSafeMethod()
        {
            List<int> listInt = new List<int>();
            for (int i = 0; i < 10000; i++)
            {
                Task.Run(() =>
                {
                    lock (lock_Obj)
                    {
                        listInt.Add(i);
                    }
                });
            }
            Thread.Sleep(3000);
            Console.WriteLine(listInt.Count);
        }

结果

 

 

 

这里说一下为什么是锁变量推荐 private static readonly 

私有是防止有其他代码方法这个锁变量,自己的锁就自己用,否则有可能形成不同的功能之间的互相竞争,功能之间无法并发,形成阻塞。

静态是确保不同的实例调用的是同一个锁变量,如果不是静态,在一段代码中实例化多次这个类的对象时,每次的锁都是新的,这样多次实例化的对象调用加锁的方法,它们之间会形成并发。

只读,如果不是只读,有人把锁变量重新改了,那之间的锁就失效了,因为引用已经变了。

为啥不能用String当锁变量呢?因为String类型比较特殊,多个string如果赋值相同的话,不会被视为不同的对象,而是通过同样的引用指向同一个堆的地址,这样你用这多个string当锁变量的话,互相之间会竞争,无法并发。

 

使用Lock,其实就是保证被它包起来的代码块,同一时刻只能有一个线程访问,其他要排队,说白了,就是单线程化。

 

posted @ 2021-01-05 21:05  luytest  阅读(232)  评论(0编辑  收藏  举报