C# 中 ConcurrentDictionary 一定线程安全吗?
2020-12-22 12:01
1336 查看
根据 .NET 官方文档的定义:
ConcurrentDictionary<TKey,TValue>Class 表示可由多个线程同时访问的线程安全的键/值对集合。这也是我们在并发任务中比较常用的一个类型,但它真的是绝对线程安全的吗?
仔细阅读官方文档,我们会发现在文档的底部线程安全性小节里这样描述:
ConcurrentDictionary<TKey,TValue>的所有公共和受保护的成员都是线程安全的,可从多个线程并发使用。但是,通过一个由ConcurrentDictionary<TKey,TValue>实现的接口的成员(包括扩展方法)访问时,不保证其线程安全性,并且可能需要由调用方进行同步。
也就是说,调用 ConcurrentDictionary 本身的方法和属性可以保证都是线程安全的。但是由于 ConcurrentDictionary 实现了一些接口(例如 ICollection、IEnumerable 和 IDictionary 等),使用这些接口的成员(或者这些接口的扩展方法)不能保证其线程安全性。
System.Linq.Enumerable.ToList方法就是其中的一个例子,该方法是
IEnumerable的一个扩展方法,在 ConcurrentDictionary 实例上使用该方法,当它被其它线程改变时可能抛出
System.ArgumentException异常。下面是一个简单的示例:
static void Main(string[] args) { var cd = new ConcurrentDictionary<int, int>(); Task.Run(() => { var random = new Random(); while (true) { var value = random.Next(10000); cd.AddOrUpdate(value, value, (key, oldValue) => value); } }); while (true) { cd.ToList(); //调用 System.Linq.Enumerable.ToList,抛出 System.ArgumentException 异常 } }
System.Linq.Enumerable.ToList扩展方法:
发生异常是因为扩展方法
ToList中调用了
List的构造函数,该构造函数接收一个
IEnumerable<T>类型的参数,且该构造函数中有一个对
ICollection<T>的优化(由 ConcurrentDictionary 实现的)。
System.Collections.Generic.List<T>构造函数:
在
List的构造函数中,首先通过调用
Count获取字典的大小,然后以该大小初始化数组,最后调用
CopyTo将所有
KeyValuePair项从字典复制到该数组。因为字典是可以由多个线程改变的,在调用
Count后且调用
CopyTo前,字典的大小可以增加或者减少。当
ConcurrentDictionary试图访问数组超出其边界时,将引发
ArgumentException异常。
ConcurrentDictionary<TKey,TValue> 中实现的 ICollection.CopyTo 方法:
如果您只需要一个包含字典所有项的单独集合,可以通过调用
ConcurrentDictionary.ToArray方法来避免此异常。它完成类似的操作,但是操作之前先获取了字典的所有内部锁,保证了线程安全性。
注意,不要将此方法与
System.Linq.Enumerable.ToArray扩展方法混淆,调用
Enumerable.ToArray像
Enumerable.ToList一样,可能引发
System.ArgumentException异常。
看下面的代码中:
static void Main(string[] args) { var cd = new ConcurrentDictionary<int, int>(); Task.Run(() => { 1044 var random = new Random(); while (true) { var value = random.Next(10000); cd.AddOrUpdate(value, value, (key, oldValue) => value); } }); while (true) { cd.ToArray(); //ConcurrentDictionary.ToArray, OK. } }
此时调用
ConcurrentDictionary.ToArray,而不是调用
Enumerable.ToArray,因为后者是一个扩展方法,前者重载解析的优先级高于后者。所以这段代码不会抛出异常。
但是,如果通过字典实现的接口(继承自 IEnumerable)使用字典,将会调用
Enumerable.ToArray方法并抛出异常。例如,下面的代码显式地将
ConcurrentDictionary实例分配给一个
IDictionary变量:
static void Main(string[] args) { System.Collections.Generic.IDictionary<int, int> cd = new ConcurrentDictionary<int, int>(); Task.Run(() => { var random = new Random(); while (true) { var value = random.Next(10000); cd[value] = value; } }); while (true) { cd.ToArray(); //调用 System.Linq.Enumerable.ToArray,抛出 System.ArgumentException 异常 } }
此时调用
Enumerable.ToArray,就像调用
Enumerable.ToList时一样,引发了
System.ArgumentException异常。
总结
正如官方文档上所说的那样,ConcurrentDictionary 的所有公共和受保护的成员都是线程安全的,可从多个线程并发调用。但是,通过一个由 ConcurrentDictionary 实现的接口的成员(包括扩展方法)访问时,并不是线程安全的,此时要特别注意。
如果需要一个包含字典所有项的单独集合,可以通过调用
ConcurrentDictionary.ToArray方法得到,千万不能使用扩展方法
ToList,因为它不是线程安全的。
参考:
- http://blog.i3arnon.com/2018/01/16/concurrent-dictionary-tolist/ ConcurrentDictionary Is Not Always Thread-Safe
- https://docs.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2 ConcurrentDictionary<TKey,TValue> Class
作者 : 技术译民
出品 : 技术译站
相关文章推荐
- C#怎样让WebBrowser滚动条自动上下滚动一定时间
- C# 两行代码实现 延迟加载的单例模式(线程安全)
- C#集合类(HashTable, Dictionary, ArrayList)与HashTable线程安全
- C#线程安全使用(三)
- C#匿名方法的应用二则–实现异常Retry机制和WinForm线程安全访问
- c#:使用using关键字自动释放资源未必一定就会有明显好处
- 设计模式 - 线程安全的单例模式(C#)
- C#内部关于绑定事件Event的线程安全
- 思维体操:用c#简单实现按一定规则输出有序数列
- C#中的线程 -- 同步基础(同步本质,线程安全,线程中断)
- C# 线程手册 第三章 使用线程 创建线程安全的包装器(实战篇)
- 多线程下C#如何保证线程安全?
- C#内部关于绑定事件Event的线程安全
- 无锁,线程安全,延迟加载的单例实现(C#)
- c# 求一定的大阶乘
- C# 趣味小程序(3)——以一定概率取数组中的数
- C#线程安全、事件、元组的使用
- [Untiy&随机数&C#]怎样加上一个随机的数,并且在一定的范围内
- C#:窗体中控件线程安全(Control.InvokeRequired 属性)
- .NET(C#):线程安全集合的阻塞BlockingCollection的使用