工作中遇到了多个线程同时读写一个字典实例的问题,由于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类等,不然会出现多线程错误。
