Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different behaviour when collection modified between Dictionary and ConcurrentDictionary

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?

like image 328
whoami Avatar asked Mar 17 '23 01:03

whoami


1 Answers

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

like image 99
Old Fox Avatar answered Apr 07 '23 18:04

Old Fox