Different behaviour when collection modified between Dictionary and ConcurrentDictionary

风格不统一 提交于 2019-12-08 00:44:18

问题


With a normal Dictionary code as list below, I get exception that

Collection was modified; enumeration operation may not execute.

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

However with ConcurrentDictionary, this works fine.

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

Why there is difference in behaviour?


回答1:


The reason is Dictionary and ConcurrentDictionary have different Purposes. ConcurrentDictionary - supposed to deals with concurrency issues(editing from different threads) while Dictionary will give you a better performance.

The reason to the different behavior is: different implementation of GetEnumerator() method.

Now I will explain the reason for the exception with Dictionary and the reason you do not get exception with ConcurrentDictionary.

The foreach statement is syntactic sugar for something like:

    var f = dict.GetEnumerator();

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

            // your logic
        }

The "GetEnumerator()" in Dictionary returns new instance of struct named: "Enumerator"

This structure implements: IEnumerator >KeyValuePair>TKey,TValue>>, IDictionaryEnumerator and his C'tor looks like:

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

The implementation of MoveNext() inside "Enumerator" verify first that the source Dictionary wasn't modified:

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

The "GetEnumerator()" in ConcurrentDictionary implemented a way different:

   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;
             }
         }
    }

In this implementation there is a technic called "lazy evaluation" the return statement will return the value. When the consumer calls MoveNext() then you will return to "current = current.m_next;" So, there is no "not change" verification inside GetEnumerator().

if you want to avoid exception with the "Dictionary editing" then: 1. iterate to the element you want to remove 2. remove the element 3. break before MoveNext() called

In your example:

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

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

            break; // will prevent from exception
        }

for more information about the GetEnumerator() of ConcurrentDictionary: https://msdn.microsoft.com/en-us/library/dd287131(v=vs.110).aspx




回答2:


The purpose of a ConcurrentDictionary is to allow for multiple threads to use it with a minimum of locking. If a thread wishes to receive from a typical concurrent data structure an enumeration which represents an exact combination of data that were held at some moment in time, it will be necessary to use a lock to ensure that no updates can take place while a snapshot of the structure is constructed. Even when using ConcurrentDictionary, code that wanted to construct such a shapshot could use such an approach.

In many cases, however, code will be happy with any enumeration which meets all of the following criteria:

  • The enumeration will include all data items that exist before enumeration continue to exist, without modification, throughout enumeration.

  • The enumeration will not include any data items which the collection does not contain at any time during the enumeration.

  • If the collection does not contain an item when enumeration starts, but the item is added and/or modified N times during enumeration, enumeration shall report the item no more than N times.

  • If the collection contains an item when enumeration starts, and the item is added and/or modified N times during enumeration, enumeration shall report the item no more than N+1 times.

The cost of an enumeration method meeting the above criteria may be cheaper than one which needs to return a "snapshot"; since such enumerations are often useful, ConcurrentDictionary defines its GetEnumerator method to return the cheaper one. Such behavior would not prevent code from using external locking if so inclined, but if the only available enumerator always took a snapshot, there would be no way for code to use a higher-performance enumeration when a precise snapshot wasn't required.

PS--I happen to think that it would have been helpful for ConcurrentDictionary to have included some means of explicitly requesting an enumerable snapshot of its contents, even if taking such a snapshot would be relatively slow and would block some or all concurrent accesses. Even if snapshots of a large collection were too slow to be used frequently, having a true snapshot of a collection can be useful in many debugging scenarios.



来源:https://stackoverflow.com/questions/28480150/different-behaviour-when-collection-modified-between-dictionary-and-concurrentdi

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!