C# 根据类的当前实现,通过直接枚举ConcurrentDictionary,将ConcurrentDictionary复制到普通字典是否安全?
TL;DR:一个C# 根据类的当前实现,通过直接枚举ConcurrentDictionary,将ConcurrentDictionary复制到普通字典是否安全?,c#,multithreading,dictionary,concurrentdictionary,C#,Multithreading,Dictionary,Concurrentdictionary,TL;DR:一个ConcurrentDictionary的单个枚举是否可以发出相同的键两次?ConcurrentDictionary类(.NET 5)的属性是否允许这种可能性 我有一个由多个线程同时变异的ConcurrentDictionary,我希望定期将其复制到一个普通的字典,并将其传递到表示层以更新UI。有两种方法可以复制它,有快照语义和没有快照语义: var concurrent = new ConcurrentDictionary<string, decimal>();
ConcurrentDictionary
的单个枚举是否可以发出相同的键两次?ConcurrentDictionary
类(.NET 5)的属性是否允许这种可能性
我有一个由多个线程同时变异的
ConcurrentDictionary
,我希望定期将其复制到一个普通的字典
,并将其传递到表示层以更新UI。有两种方法可以复制它,有快照语义和没有快照语义:
var concurrent = new ConcurrentDictionary<string, decimal>();
var copy1 = new Dictionary<string, decimal>(concurrent.ToArray()); // Snapshot
var copy2 = new Dictionary<string, decimal>(concurrent); // On-the-go
var concurrent=新建ConcurrentDictionary();
var copy1=新字典(concurrent.ToArray());//快照
var copy2=新字典(并发);//忙碌
我非常确定第一种方法是安全的,因为该方法返回一致的ConcurrentDictionary视图
:
返回一个新数组,其中包含从ConcurrentDictionary
复制的键和值对的快照
但我更愿意使用第二种方法,因为它产生的争用更少。
但是我担心会出现参数异常:已经添加了具有相同密钥的项。
似乎没有排除这种可能性:
枚举数从字典返回。。。不表示字典的即时快照。通过枚举器公开的内容可能包含调用GetEnumerator
后对词典所做的修改
以下是让我担心的情景:
ConcurrentDictionary
,并且枚举器发出键X
。然后线程被操作系统暂时挂起X
X
添加一个新条目ConcurrentDictionary
,枚举器观察新添加的X
条目,并将其发出Dictionary
类的构造函数尝试将键X
插入两次新构造的Dictionary
,并引发异常ConcurrentDictionary
的快速副本是安全的,还是应该防御性地编写代码并在每次复制时拍摄快照
澄清:我同意任何人的说法,即使用API时考虑其未记录的实现细节是不明智的。但是,唉,这就是这个问题的全部内容。这是一个很有教育意义的问题,出于好奇。我保证,我不打算在生产代码中使用所获得的知识。 实际上,ConcurrentDictionary的单个枚举是否可能发出相同的键两次 这取决于你如何定义“实践”。但根据我的定义,是的,在实践中,
concurrentdirectionary
完全有可能发出相同的密钥两次。也就是说,您不能编写正确的代码来假设它不会
:
通过枚举器公开的内容可能包含调用GetEnumerator后对字典所做的修改
它不提供关于行为的其他语句,这意味着调用GetEnumerator()
时可能存在一个键,该键由第一个枚举元素返回,然后删除,然后以允许枚举器再次检索相同键的方式再次添加
这是我们在实践中唯一可以依靠的东西
现在,也就是说,从学术角度讲(即不在实践中)
ConcurrentDictionary类(.NET 5)的当前实现是否允许这种可能性
在对的检查中,我认为当前的实现可能避免多次返回同一密钥的可能性
根据代码中的注释,其内容如下:
//提供此迭代器的手动实现版本(大约):
//节点?[]存储桶=_个表。_个存储桶;
//for(int i=0;i
然后看看注释所指的“手动实现的版本”…我们可以看到,实现只不过是迭代bucket
数组,然后在每个bucket中,迭代构成该bucket的链表,正如注释中的示例代码所示
但是看看,我们看到:
//在存储桶中找不到密钥。插入键值对。
var resultNode=新节点(键、值、hashcode、bucket);
Volatile.Write(ref bucket,resultNode);
选中的
{
表._countPerLock[lockNo]++;
}
当然,这个方法还有很多,但这是关键。此代码将bucket
列表的头部传递给新节点构造函数,新节点构造函数又将新节点插入列表的头部。然后bucket
变量(它是ref
变量)被新节点引用覆盖
即,新节点成为新节点的新头