C# 根据类的当前实现,通过直接枚举ConcurrentDictionary,将ConcurrentDictionary复制到普通字典是否安全?

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>();

TL;DR:一个
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
后对词典所做的修改

以下是让我担心的情景:

  • 线程A开始枚举
    ConcurrentDictionary
    ,并且枚举器发出键
    X
    。然后线程被操作系统暂时挂起
  • 线程B移除键
    X
  • 线程C使用键
    X
    添加一个新条目
  • 线程A继续枚举
    ConcurrentDictionary
    ,枚举器观察新添加的
    X
    条目,并将其发出
  • Dictionary
    类的构造函数尝试将键
    X
    插入两次新构造的
    Dictionary
    ,并引发异常
  • 我试图重现这种情景,但没有成功。但这并不是100%令人放心,因为可能导致这种情况出现的条件可能很微妙。可能我添加的值没有“正确”的哈希代码,或者没有生成“正确”数量的哈希代码冲突。我试图通过研究这门课的内容来找到答案,但不幸的是,它太复杂了,我无法理解

    我的问题是:基于当前的实现(.NET 5),通过直接枚举来创建我的
    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
    变量)被新节点引用覆盖

    即,新节点成为新节点的新头