发布博文
随想
读书
音乐
其他
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。
我想爱,想吃,还想在一瞬间变成天上半明半暗的云。

线程安全的字典 ConcurrentDictionary类使用方法简析

1121
高光翔
2018-06-10 22:18

timg.jpg

  工作中遇到了多个线程同时读写一个字典实例的问题,由于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();
         }
     }
 }

  运行代码后,输出结果如下图所示。

捕获.png

   大家需要注意的是,System.Collections.Concurrent命名空间并没有提供线程安全的List集合,因此如果多线程读写List集合,需要自己写代码处理同步,如使用Mutex类等,不然会出现多线程错误。

Insert title here Insert title here
打  赏