我遇到了一个有趣的问题。知道ConcurrentDictionary<TKey, TValue>
在被修改时是安全可枚举的,在(在我的情况下)迭代可能消失或多次出现的元素的不必要的副作用,我决定自己创建一个快照,使用{{1 }}。由于ToList()
也会实现ConcurrentDictionary<TKey, TValue>
,因此会导致使用ICollection<KeyValuePair<TKey, TValue>>
,而后者会使用当前项List(IEnumerable<T> collection)
在字典的当前大小中创建一个数组,然后尝试复制项目Count
,调用其using ICollection<T>.CopyTo(T[] array, int arrayIndex)
实现,最后抛出ConcurrentDictionary<TKey, TValue>
if,同时将元素添加到字典中。
全部锁定会破坏使用集合的重点,所以我的选择似乎是要么继续捕获异常并重试(这绝对不是问题的正确答案),要么实现我自己的专门针对这个问题的ArgumentException
版本(但是再一次,简单地增加一个列表然后可能将其修剪为适合几个元素的大小似乎是一种过度杀伤,并且使用LinkedList会降低索引性能)。
此外,似乎添加某些在后台创建某种缓冲区的LINQ方法(例如ToList()
)似乎以牺牲性能为代价来修复问题,但是裸{{1}显然没有,当不需要额外的功能时,不值得用另一种方法“扩充”它。
这可能是任何并发收集的问题吗?
在创建此类快照时,将性能命中率保持在最低水平的合理解决方法是什么? (最好在一些LINQ魔术结束时。)
修改
在查看之后我可以确认,OrderBy
(认为我昨天刚刚通过它)确实解决了快照问题,只要它只是一个简单的快照,它在附加时没有帮助在获取所述快照(例如过滤,排序)之前需要功能,并且最后仍需要列表/数组。 (在这种情况下,需要额外调用,重新创建新集合。)
我没有指出快照可能需要也可能不需要经过这些修改,所以应该在最后进行,最好是,所以我会将这些添加到问题中。
(另外,如果有人对标题有更好的了解,请告诉。)
答案 0 :(得分:6)
让我们回答所有并发类型的广泛过度遮蔽问题:
如果您拆分了多个步骤处理内部的操作,其中所有步骤必须&#34;同步&#34;,然后是,最终您将崩溃并且由于线程同步导致奇数结果。
因此,如果使用.ToList()
首先要求.Count
,请调整数组大小,然后使用foreach
获取值并放入列表中,然后是< / strong>,最终你将有两个部分获得不同数量元素的机会。
说实话,我希望其中一些并发类型没有试图通过实现很多这些接口来假装它们是正常的集合,但唉,这是怎么回事。
现在你知道这个问题,你可以修复你的代码吗?
是的,您可以,您必须查看类型文档,看看它是否提供了任何形式的快照机制,不容易出现上述问题。
结果ConcurrentDictionary<TKey, TValue>
实现.ToArray()
,documented为:
包含从System.Collections.Concurrent.ConcurrentDictionary复制的键和值对的快照的新数组。
(我的重点)
.ToArray()
目前是如何实施的?
Using locks,见第697行。
因此,如果您觉得锁定整个字典以获取快照成本太高,我会质疑抓住其内容快照的行为。
此外,.GetEnumerator()
方法遵循一些相同的规则,来自documentation:
字典中返回的枚举器可以安全地同时使用,对字典进行读写操作,但它不代表字典的时刻快照。通过枚举器公开的内容可能包含在调用GetEnumerator后对字典进行的修改。
(再次,我的emhpasis)
因此,虽然.GetEnumerator()
赢得崩溃,但它可能无法产生您想要的结果。
根据时间安排,.ToArray()
都不是,所以一切都取决于。