上一期,C#关于List的线程安全问题(一)我们给出了一个线程不安全的例子。
这个例子给人的感觉就是总觉得哪里不对,命名插入5000个数据到List中,结果却并不是自己想要的。
明明一共插入了1300个数据,结果也不是。
这都是因为List默认线程不安全导致的,也就是当某一个线程正在往List中插入数据,结果由于其他线程也正在做插入动作,导致冲突,插入可能失败,并且插入的顺序是不可控的,除非我们并不关心插入的顺序。
那么我们怎么样才能让结果是我们所需要的呢?
这里我们引入ICollection接口(List<T>是拥有ICollection接口的),这个接口当中有个叫SyncRoot的属性,它是个辅助属性,它是微软实现为我们准备好了拿来做同步用的,也就是拿来做线程安全用的。它一般用于lock语句块。用来将集合资源锁定,可用于同步对ICollection的访问的对象。
public object SyncRoot { get; }
它是一个实例化的属性,即每个List实例对象都会有各自的一个SyncRoot。如果想安全访问集合对象,我们可以如下使用:
ICollection myCollection = someCollection;
lock(myCollection.SyncRoot)
{
// Some operation on the collection, which is now thread safe.
}
所以C#关于List的线程安全问题(一)中的例子,我们可以稍加修改如下:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;
namespace vscode_test2
{
class Program
{
static void Main(string[] args)
{
List<int> mylist = new List<int>();
var t = Task.Run(()=>{
Thread.Sleep(2000);
lock((mylist as ICollection).SyncRoot)
{
for(int i=0; i<8000; i++)
{
mylist.Add(3);
Thread.Sleep(1);
}
System.Console.WriteLine($"task: list size:{mylist.Count}");
}
});
Thread.Sleep(2000);
lock((mylist as ICollection).SyncRoot)
{
for(int i=0; i<5000; i++)
{
Thread.Sleep(1);
mylist.Add(6);
}
System.Console.WriteLine($"main: list size:{mylist.Count}");
}
t.Wait();
Console.Read();
}
}
}
/*
main: list size:5000
task: list size:13000
*/
这是输出结果,就是我们想要的。当然这个想要的,是如您业务所愿。如果你是无所谓插入顺序或者插入是否失败,那么代码随你写。因此,安不安全,同步与否是需要有业务逻辑来定的。反正,如果我们想同步,那么微软已经为我们准备了一个叫做SyncRoot属性,我们不需要为了同步某个Collection集合额外定义一个锁对象,集合对象内部就已经替我们准备了一把叫做SyncRoot的锁。
同样的,其他的Collection类,也可以用这个方式实现线程安全。