C# 中 ConcurrentDictionary 一定线程安全吗?
根据 .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(() =>
{
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# 中 ConcurrentDictionary 一定线程安全吗?的更多相关文章
- LoadTest中内存和线程Troubleshooting实战
LoadTest中内存和线程Troubleshooting实战 在端午节放假的三天中,我对正在开发的Service进行了LoadTest,尝试在增大压力的条件下发现问题. 该Service为独立进程的 ...
- 重新想象 Windows 8 Store Apps (42) - 多线程之线程池: 延迟执行, 周期执行, 在线程池中找一个线程去执行指定的方法
[源码下载] 重新想象 Windows 8 Store Apps (42) - 多线程之线程池: 延迟执行, 周期执行, 在线程池中找一个线程去执行指定的方法 作者:webabcd 介绍重新想象 Wi ...
- Java中的守护线程和非守护线程(转载)
<什么是守护线程,什么是非守护线程> Java有两种Thread:"守护线程Daemon"(守护线程)与"用户线程User"(非守护线程). 用户线 ...
- springmvc中request的线程安全问题
SpringMvc学习心得(四)springmvc中request的线程安全问题 标签: springspring mvc框架线程安全 2016-03-19 11:25 611人阅读 评论(1) 收藏 ...
- Unity 中 使用c#线程
使用条件 天下没有免费的午餐,在我使用unity的那一刻,我就感觉到不自在,因为开源所以不知道底层实现,如果只是简单的做点简单游戏,那就无所谓的了,但真正用到实际地方的时候,就会发现一个挨着一个坑 ...
- Java中的守护线程 & 非守护线程(简介)
Java中的守护线程 & 非守护线程 守护线程 (Daemon Thread) 非守护线程,又称用户线程(User Thread) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守 ...
- HttpApplication中的异步线程
一.Asp.net中的线程池设置 在Asp.net的服务处理中,每当服务器收到一个请求,HttpRuntime将从HttpApplication池中获取一个HttpApplication对象处理此请求 ...
- c#中如何跨线程调用windows窗体控件
c#中如何跨线程调用windows窗体控件? 我们在做winform应用的时候,大部分情况下都会碰到使用多线程控制界面上控件信息的问题.然而我们并不能用传统方法来做这个问题,下面我将详细的介绍.首 ...
- c#中如何跨线程调用windows窗体控件?
我们在做winform应用的时候,大部分情况下都会碰到使用多线程控制界面上控件信息的问题.然而我们并不能用传统方法来做这个问题,下面我将详细的介绍.首先来看传统方法: public partial c ...
随机推荐
- C# redis集群批量操作之slot计算出16384个字符串
引入一个大家都用的到的需求来说吧. 需求:要在三主三从的redis集群,存入数据,会对数据进行批量删除操作,数据要求要在redis集群负载均衡. 思路: 1.存入数据好办 1 var connect ...
- ppt-1 操作界面与基本操作
1.Ctrl+N快速建立新文档 2.新模板:文件--新建--可免费搜索.下载新模板 3.恢复未保存的演示文稿 文件--打开(首先看到的是近期使用的演示文稿,)--鼠标滚动至末尾,可看到"恢复 ...
- symfony框架学习
http://mozilla.com.cn/thread-52722-1-1.html安装网页翻译插件 symfony2是基于php的web框架 http://www.chrisyue.com/sym ...
- [C#.NET 拾遗补漏]13:动态构建LINQ查询表达式
最近工作中遇到一个这样的需求:在某个列表查询功能中,可以选择某个数字列(如商品单价.当天销售额.当月销售额等),再选择 小于或等于 和 大于或等于 ,再填写一个待比较的数值,对数据进行查询过滤. 如果 ...
- nmap安装和使用
nmap安装和使用 安装 官网地址 https://nmap.org/download.html 许多流行的Linux发行版(Redhat.Mandrake.Suse等)都使用RPM软件包管理系统,方 ...
- Java基础教程——JUnit和TDD
JUnit JUnit是一个Java的单元测试工具. package ah; public class MyCode { public int m1() { System.out.println(&q ...
- dubbo与zk
一.总体流程: 1.服务提供者启动时,会向注册中心写入自己的元数据信息,同时会订阅配置元数据信息: 2.消费者启动时,也会向注册中心写入自己的元数据信息,并订阅服务提供者.路由和配置元数据信息: 3. ...
- VisualStudio 编写汇编代码相关设置
VS编写汇编代码方法 新建空项目,不创建解决方案 项目右键,Build Customizations,选择masm 新建源文件,后缀为.ASM 编写代码 .386 ; Tells MASM to us ...
- 痞子衡嵌入式:一个奇怪的Keil MDK下变量链接强制对齐报错问题(--legacyalign)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是一个奇怪的Keil MDK下变量链接强制对齐报错问题. 痞子衡最近一直在参与恩智浦SBL项目(就是一个适用LPC和i.MXRT的完整OT ...
- 这些鲜为人知的前端冷知识,你都GET了吗?
背景 最近公司项目不多,比较清闲,划水摸鱼混迹于各大技术博客平台,瞬间又GET了好多前端技能,一些属于技巧,一些则是闻所未闻的冷知识,一时间还消化不过来,不由的发出一声感叹! 前端可真是博大精深 于是 ...