2015-02-12 3 views
1

С нормальным Словарем кодом в список ниже, я получаю исключение,Различного поведение, когда коллекция модифицирована между Dictionary и 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); 
} 

Почему существует разница в поведении?

+0

Потому что они разные классы, они ведут себя иначе. 'ConcurrentDictionary' предназначен для использования с несколькими потоками, где можно удалить элемент, а другой - итерации, и, таким образом, он не генерирует исключение. –

+0

Я знаю, что это разные классы, но я думал, что концепция только для чтения относится ко всем классам сбора. – BKS

+0

Нет, не все классы вызывают исключение, если модифицировать его как перечисление. Исключениями являются классы в пространстве имен 'System.Collections.Concurrent'. Также 'ControlCollection' в winforms - это тот, который я помню, который не выбрасывает это исключение. –

ответ

3

Причина в Словаре и ConcurrentDictionary имеет разные цели. ConcurrentDictionary - предполагается решать проблемы параллелизма (редактирование из разных потоков), в то время как словарь даст вам лучшую производительность.

Причиной различного поведения является: различная реализация метода GetEnumerator().

Теперь я объясню причину исключения в словаре и причину, по которой вы не получите исключение из ConcurrentDictionary.

Еогеасп заявление синтаксический сахар что-то вроде:

var f = dict.GetEnumerator(); 

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

      // your logic 
     } 

"GetEnumerator()" в Словаре возвращает новый экземпляр структуры под названием: "Enumerator"

Эта структура реализует: IEnumerator> KeyValuePair > TKey, TValue >>, 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>(); 
     } 

реализация MoveNext() внутри "Enumerator" проверить первый что исходный словарь не был изменен:

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

«GetEnumerator()» в ConcurrentDictionary реализован способ другое:

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

В этой реализации есть техника под названием «ленивые вычисления» возвращение оператор вернет значение. Когда потребитель вызывает 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 
     } 

для получения дополнительной информации о GetEnumerator() из ConcurrentDictionary: https://msdn.microsoft.com/en-us/library/dd287131(v=vs.110).aspx

2

Th Цель ConcurrentDictionary состоит в том, чтобы разрешить нескольким потокам использовать его с минимальной блокировкой.Если поток хочет получить из типичной параллельной структуры данных перечисление, которое представляет точную комбинацию данных, которые были сохранены в какой-то момент времени, необходимо будет использовать блокировку, чтобы гарантировать, что никакие обновления не могут произойти во время моментального снимка структура построена. Даже при использовании ConcurrentDictionary код, который хотел бы построить такой шапшот, мог бы использовать такой подход.

Во многих случаях, однако, код будет доволен любое перечисление, которое отвечает всем следующим критериям:

  • Перечисление будет включать в себя все элементы данных, которые существуют до того перечисления продолжают существовать, без модификация, на протяжении всего перечисления.

  • Перечисление не будет содержать элементы данных, которые коллекция не содержит в любое время во время перечисления.

  • Если коллекция не содержит элемент при запуске перечисления, но элемент добавляется и/или изменяется N раз во время перечисления, перечисление должно сообщать об этом изделии не более N раз.

  • Если коллекция содержит элемент при запуске нумерации, и элемент добавляется и/или изменяется N раз во время перечисления, перечисление должно сообщать об этом изделии не более N + 1 раз.

Стоимость метода перечисления, отвечающего вышеуказанным критериям, может быть дешевле той, которая должна вернуть «моментальный снимок»; так как такие перечисления часто полезны, ConcurrentDictionary определяет его метод GetEnumerator для возврата более дешевого. Такое поведение не помешало бы коду использовать внешнюю блокировку, если бы это было наклонно, но если единственный доступный перечислитель всегда делал моментальный снимок, не было бы способа использовать код для более высокопроизводительной переписи, когда точный снимок не требовался.

PS - Мне кажется, что было бы полезно, если бы ConcurrentDictionary включил некоторые способы явного запроса перечислимого моментального снимка его содержимого, даже если такой снимок будет относительно медленным и блокирует некоторые или все одновременный доступ. Даже если снимки большой коллекции были слишком медленными, чтобы их можно было использовать часто, наличие моментального снимка коллекции может быть полезно во многих сценариях отладки.

 Смежные вопросы

  • Нет связанных вопросов^_^