异步多线程 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,其实就是保证被它包起来的代码块,同一时刻只能有一个线程访问,其他要排队,说白了,就是单线程化。