Вызов ToList() для ConcurrentDictionary‹TKey, TValue› при добавлении элементов

Я столкнулся с интересной проблемой. Зная, что ConcurrentDictionary<TKey, TValue> можно безопасно перечислить при изменении, с (в моем случае) нежелательным побочным эффектом перебора элементов, которые могут исчезать или появляться несколько раз, я решил создать снимок самостоятельно, используя ToList(). Поскольку ConcurrentDictionary<TKey, TValue> также реализует ICollection<KeyValuePair<TKey, TValue>>, это приводит к использованию List(IEnumerable<T> collection), который, в свою очередь, создает массив текущего размера словаря, используя текущий элемент Count, затем пытается скопировать элементы using ICollection<T>.CopyTo(T[] array, int arrayIndex), вызывая его реализацию ConcurrentDictionary<TKey, TValue>, и, наконец, бросая ArgumentException, если элементы тем временем добавляются в словарь.

Полная блокировка убьет смысл использования коллекции как таковой, поэтому мои варианты, похоже, заключаются в том, чтобы либо продолжать перехватывать исключение и повторять попытку (что определенно не является правильным решением проблемы), либо реализовать мою собственную версию ToList() специализированный для этой проблемы (но опять же, простое увеличение списка, а затем, возможно, его обрезка до нужного размера для нескольких элементов кажется излишним, а использование LinkedList снизит производительность индексации).

Кроме того, кажется, что добавление определенных методов LINQ, создающих какой-то буфер в фоновом режиме (например, OrderBy), кажется, решает проблему за счет производительности, но голый ToList() явно этого не делает, да и не стоит" дополняя его другим методом, когда дополнительные функции не требуются.

Может ли это быть проблемой с какой-либо параллельной коллекцией?

Что было бы разумным обходным путем, чтобы свести к минимуму потери производительности при создании такого моментального снимка? (Желательно в конце некоторой магии LINQ.)

Изменить:

Изучив его, я могу подтвердить, ToArray() (думать, что я только что прошел мимо него вчера) действительно решает проблему снимка, если это просто простой снимок, он не помогает, когда требуются дополнительные функции, прежде чем сделать указанный снимок (например, фильтрация, сортировка), и список/массив по-прежнему необходим в конце. (В этом случае требуется дополнительный вызов, создающий новую коллекцию заново.)

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

(Кроме того, если у кого-то есть лучшая идея для названия, скажите.)


person Roland Szakacs    schedule 08.12.2016    source источник
comment
Вместо этого используйте .ToArray(), который реализован специально для типа.   -  person Lasse V. Karlsen    schedule 08.12.2016


Ответы (1)


Давайте ответим здесь на широкий вопрос перекрытия для всех одновременных типов:

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

Таким образом, если при использовании .ToList() сначала запрашивается .Count, затем размер массива, а затем используется foreach для получения значений и размещения в списке, тогда да, у вас определенно будет шанс получить две части разное количество элементов.

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

Теперь, когда вы знаете о проблеме, можете ли вы исправить свой код?

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

Оказывается, ConcurrentDictionary<TKey, TValue> реализует .ToArray(), что задокументировано с:

Новый массив, содержащий моментальный снимок пар ключ-значение, скопированный из System.Collections.Concurrent.ConcurrentDictionary.

(выделено мной)

Как сейчас реализуется .ToArray()?

Использование блокировок, см. строку 697.

Поэтому, если вы считаете, что блокировка всего словаря для получения моментального снимка слишком затратна, я бы поставил под сомнение акт получения моментального снимка его содержимого для начала.

Кроме того, метод .GetEnumerator() следует некоторым из тех же правил из документация:

Перечислитель, возвращенный из словаря, безопасен для использования одновременно с операциями чтения и записи в словарь, однако он не представляет моментальный снимок словаря. Содержимое, отображаемое через перечислитель, может содержать изменения, внесенные в словарь после вызова GetEnumerator.

(опять же, мой акцент)

Таким образом, хотя .GetEnumerator() не будет зависать, он может не дать желаемых результатов.

В зависимости от времени ни один из них не может .ToArray(), так что все зависит.

person Lasse V. Karlsen    schedule 08.12.2016