工作中遇到了多个线程同时读写一个字典实例的问题,由于Dictionary类不是线程安全的,如果多个线程同时读写一个Dictionary的实例,会引发多线程错误,.net框架提供了一个线程安全的字典类,ConcurrentDictionary,属于 System.Collections.Concurrent的命名空间,该命名空间下还提供了ConcurrentBag(背包)、ConcurrentQueue(队列)、ConcurrentStack(栈
)等线程安全的数据结构类。下面我主要介绍下最常用的AddOrUpdate方法,有两个重载方法。
名称 | 说明 |
AddOrUpdate(TKey, TValue, Func<TKey, TValue, TValue>) | 如果该键不存在,则将键/值对添加到 ConcurrentDictionary<TKey, TValue> 中;如果该键已经存在,则通过使用指定的函数更新 ConcurrentDictionary<TKey, TValue> 中的键/值对。 |
AddOrUpdate(TKey, Func<TKey, TValue>, Func<TKey, TValue, TValue>) | 如果该键不存在,则使用指定函数将键/值对添加到 ConcurrentDictionary<TKey, TValue>;如果该键已存在,则使用该函数更新 ConcurrentDictionary<TKey, TValue> 中的键/值对。 |
下面我将以一个例子主要介绍下重载的第二个方法的使用,第一个方法类似。比如新生入学了,我要建立一个班级和班级人员的字典集合,key为班级名称,数据类型为string,value为班级人员的集合,我们选用ConcurrentQueue<string>来存储班级人员的名字(此处选用队列或许不太合适,但List<string>并非线程安全的,需要额外的代码控制同步,为了简便,我们选用线程安全的队列,可以暂且认为按新生注册顺序排队)。
namespace MyBlogProject { class DivideClassProcessor { public static ConcurrentDictionary<string, ConcurrentQueue<string>> ClassDic = new ConcurrentDictionary<string, ConcurrentQueue<string>>(); /// <summary> /// 输入班级名称和学生名字,将学生加入相应的班级 /// </summary> /// <param name="className">班级名字</param> /// <param name="studentName">学生名字</param> public void DvideClass(string className,string studentName) { ClassDic.AddOrUpdate(className, (cn) => { ConcurrentQueue<string> studentQueue = new ConcurrentQueue<string>(); studentQueue.Enqueue(studentName); return studentQueue; }, (cn, studentQueue) => { studentQueue.Enqueue(studentName); return studentQueue; }); } } }
下面分别说明一下传入的三个参数,className为班级名称,为字典的key值,第二个参数lamda表达式为字典执行Add时调用的委托方法,如果字典中尚且没有这个班级的key,就执行这个lamda表达式,新建了一个班级人员队列,将班级和队列添加到字典中,然后将这个新同学入队。第三个参数的lamda表达式为字典执行Update时调用的方法,即字典中已经存在了这个班级的键值对,studentQueue为已有的班级建所对应的学生队列,然后我将新同学的名字入队,因为不需要改变新的学生集合队列,还将已有的学生队列返回。这样就完成了学生分班入队的功能。
因为多个线程在操作这个字典实例,所以执行过程中到底是执行的Add还是Update,我们是不知道的,只能将两个委托方法都给出来,到时候封装好的ConcurrentDictionary会同步去判断到底该执行哪个,因此在委托方法中不需要再进行是否含有key值的判断。
下面的代码模拟多线程将学生和班级信息存入字典中,并输出到屏幕。
namespace MyBlogProject { class Program { static void Main(string[] args) { DivideClassProcessor processor = new DivideClassProcessor(); List<Task> taskList = new List<Task>(); //分别建立入队的线程 Task devideClassTask = new Task(() => { processor.DvideClass("一班", "张晓章"); }); taskList.Add(devideClassTask); devideClassTask = new Task(() => { processor.DvideClass("一班", "王晓旺"); }); taskList.Add(devideClassTask); devideClassTask = new Task(() => { processor.DvideClass("二班", "李晓里"); }); taskList.Add(devideClassTask); devideClassTask = new Task(() => { processor.DvideClass("二班", "高晓糕"); }); taskList.Add(devideClassTask); //同时开启分班线程 foreach(Task task in taskList) { task.Start(); } //等所有分班线程执行完 foreach(Task task in taskList) { task.Wait(); } //输出人员 foreach(var item in DivideClassProcessor.ClassDic) { string cName = item.Key; ConcurrentQueue<string> studentQueue = item.Value; string student = ""; while(studentQueue.TryPeek(out student)) { studentQueue.TryDequeue(out student); Console.WriteLine(cName + ": " + student); } } Console.ReadLine(); } } }
运行代码后,输出结果如下图所示。
大家需要注意的是,System.Collections.Concurrent命名空间并没有提供线程安全的List集合,因此如果多线程读写List集合,需要自己写代码处理同步,如使用Mutex类等,不然会出现多线程错误。