在Dictionary和ConcurrentDictionary之间修改集合时的不同行为

时间:2015-02-12 14:33:39

标签: c# dictionary .net-4.0 enumeration concurrentdictionary

使用正常的字典代码作为下面的列表,我得到了

的异常
  

收藏被修改;枚举操作可能无法执行。

Dictionary<int, int> dict2 = new Dictionary<int, int>();
dict2.Add(1, 10);
dict2.Add(2, 20);
dict2.Add(3, 30);
dict2.Add(4, 40);

foreach (var d in dict2)
{
    if (dict2.ContainsKey(2))
        dict2.Remove(2);

    if (dict2.ContainsKey(3))
        dict2.Remove(3);
}

但是使用ConcurrentDictionary,这很好。

ConcurrentDictionary<int, int> dict1 = new ConcurrentDictionary<int, int>();
dict1.AddOrUpdate(1, 10, (k,v)=> 10);
dict1.AddOrUpdate(2, 20, (k, v) => 20);
dict1.AddOrUpdate(3, 30, (k,v)=> 30);
dict1.AddOrUpdate(4, 40, (k,v)=> 40);

foreach (var d in dict1)
{
    int x;
    if (dict1.ContainsKey(2))
        dict1.TryRemove(2, out x);

    if (dict1.ContainsKey(3))
        dict1.TryRemove(3, out x);
}

为什么行为存在差异?

2 个答案:

答案 0 :(得分:4)

原因是Dictionary和ConcurrentDictionary有不同的目的。 ConcurrentDictionary - 应该处理并发问题(从不同的线程编辑),而Dictionary将为您提供更好的性能。

不同行为的原因是:GetEnumerator()方法的不同实现。

现在我将解释使用Dictionary的异常原因以及ConcurrentDictionary不会出现异常的原因。

foreach语句是语法糖,例如:

    var f = dict.GetEnumerator();

        while (f.MoveNext())
        {
            var x = f.Current;

            // your logic
        }

Dictionary中的“GetEnumerator()”返回名为“Enumerator”的结构的新实例

此结构实现:IEnumerator&gt; KeyValuePair&gt; TKey,TValue&gt;&gt;,IDictionaryEnumerator和他的C'tor看起来像:

        internal Enumerator(Dictionary<TKey,TValue> dictionary, int getEnumeratorRetType) {
            this.dictionary = dictionary;
            version = dictionary.version;
            index = 0;
            this.getEnumeratorRetType = getEnumeratorRetType;
            current = new KeyValuePair<TKey, TValue>();
        }

“Enumerator”中的MoveNext()实现首先验证源字典未被修改:

      bool moveNext(){
            if (version != dictionary.version) {
                throw new InvalidOperationException....
            }
            //the itarate over...
      }

ConcurrentDictionary中的“GetEnumerator()”实现了不同的方式:

   IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator(){
         Node[] buckets = m_tables.m_buckets;

         for (int i = 0; i < buckets.Length; i++)
         {

             Node current = Volatile.Read<Node>(ref buckets[i]);

             while (current != null)
             {
                 yield return new KeyValuePair<TKey, TValue>(current.m_key,  current.m_value);
                 current = current.m_next;
             }
         }
    }

在这个实现中有一个名为“lazy evaluation”的技术,return语句将返回值。 当消费者调用MoveNext()时,您将返回“current = current.m_next;” 因此,GetEnumerator()中没有“不更改”验证。

如果你想通过“字典编辑”避免异常,那么: 1.迭代到要删除的元素 2.删除元素 3.在调用MoveNext()之前中断

在你的例子中:

        foreach (var d in dict2)
        {
            if (dict2.ContainsKey(1))
                dict2.Remove(1);

            if (dict2.ContainsKey(3))
                dict2.Remove(3);

            break; // will prevent from exception
        }

有关ConcurrentDictionary的GetEnumerator()的更多信息: https://msdn.microsoft.com/en-us/library/dd287131(v=vs.110).aspx

答案 1 :(得分:2)

ConcurrentDictionary的目的是允许多个线程以最少的锁定使用它。如果线程希望从典型的并发数据结构接收枚举,该枚举表示在某个时刻保存的数据的精确组合,则需要使用锁来确保在快照时不会发生更新。结构是构造的。即使使用ConcurrentDictionary,想要构建这样一个shapshot的代码也可以使用这种方法。

但是,在许多情况下,代码会对满足以下所有条件的任何枚举感到满意:

  • 枚举将包括在枚举之前存在的所有数据项,无需修改,在整个枚举过程中。

  • 枚举不包括集合在枚举期间不包含的任何数据项。

  • 如果在枚举开始时集合中不包含项目,但在枚举期间项目被添加和/或修改N次,则枚举应报告该项目不超过N次。

  • 如果集合在枚举开始时包含项目,并且在枚举期间添加和/或修改项目N次,则枚举应报告该项目不超过N + 1次。

符合上述标准的枚举方法的成本可能比需要返回&#34;快照的成本方法便宜。由于此类枚举通常很有用,ConcurrentDictionary定义其GetEnumerator方法以返回更便宜的方法。如果这样做,这种行为不会阻止代码使用外部锁定,但如果唯一可用的枚举器总是拍摄快照,那么当不需要精确快照时,代码就无法使用更高性能的枚举。

PS - 我碰巧认为ConcurrentDictionary包含一些明确请求其内容的可枚举快照的方法会有所帮助,即使拍摄这样的快照会相对较慢并且会阻塞部分或全部并发访问。即使大型集合的快照太慢而无法频繁使用,在许多调试方案中拥有集合的真实快照也很有用。