Колекція була модифікована; Операція перерахування може не виконуватися


909

Я не можу дійти до цієї помилки, оскільки коли налагоджувач додається, він, схоже, не виникає. Нижче - код.

Це WCF-сервер у службі Windows. Метод NotifySubscribers викликається службою кожного разу, коли відбувається подія даних (через випадкові проміжки часу, але не дуже часто - приблизно 800 разів на день).

Коли клієнт Windows Forms підписується, ідентифікатор абонента додається до словника передплатників, а коли клієнт скасовує підписку, видаляється зі словника. Помилка трапляється, коли (або після) клієнт скасовує підписку. Виявляється, що наступного разу, коли викликається метод NotifySubscribers (), цикл foreach () закінчується помилкою в рядку теми. Метод записує помилку в журнал додатків, як показано в коді нижче. Коли налагоджувач додається і клієнт скасовує підписку, код виконує штраф.

Ви бачите проблему з цим кодом? Чи потрібно, щоб зробити потік словника безпечним?

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
    private static IDictionary<Guid, Subscriber> subscribers;

    public SubscriptionServer()
    {            
        subscribers = new Dictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        foreach(Subscriber s in subscribers.Values)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                UnsubscribeEvent(s.ClientId);
            }
        }
    }


    public Guid SubscribeEvent(string clientDescription)
    {
        Subscriber subscriber = new Subscriber();
        subscriber.Callback = OperationContext.Current.
                GetCallbackChannel<IDCSCallback>();

        subscribers.Add(subscriber.ClientId, subscriber);

        return subscriber.ClientId;
    }


    public void UnsubscribeEvent(Guid clientId)
    {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                    e.Message);
        }
    }
}

в моєму випадку це був побічний ефект, тому що я використовував декілька .Include ("таблицю"), які були змінені під час процесу - не дуже очевидно при читанні коду. однак мені пощастило, що ці включення не потрібні були (так! старий, без збереженого коду), і я вирішив свою проблему, просто видаливши їх
Аді,

Відповіді:


1630

Можливо, що SignalData опосередковано змінює словник підписників під кришкою під час циклу та веде до цього повідомлення. Ви можете підтвердити це, змінивши

foreach(Subscriber s in subscribers.Values)

До

foreach(Subscriber s in subscribers.Values.ToList())

Якщо я маю рацію, проблема зникне

Виклик subscribers.Values.ToList()копіює значення subscribers.Valuesв окремий список на початку foreach. Ніщо інше не має доступу до цього списку (він навіть не має назви змінної!), Тому нічого не може змінити його всередині циклу.


14
BTW .ToList () присутній у dll System.Core, який не сумісний із програмами .NET 2.0. Тому вам може знадобитися змінити цільовий додаток на .Net 3.5
mishal153

60
Я не розумію, чому ви зробили ToList і чому це все виправляє
PositiveGuy

204
@CoffeeAddict: Проблема в тому subscribers.Values, що змінюється всередині foreachциклу. Виклик subscribers.Values.ToList()копіює значення subscribers.Valuesв окремий список на початку foreach. Ніщо інше не має доступу до цього списку (він навіть не має назви змінної!) , Тому нічого не може змінити його всередині циклу.
BlueRaja - Danny Pflughoeft

31
Зауважте, що ToListможе також кинутись, якщо колекція була змінена під час ToListвиконання.
Шрірам Сактівель

16
Я впевнений, думаю, що це не вирішує проблему, а лише ускладнює її відтворення. ToListце не атомна операція. Що ще смішніше: в ToListосновному все-таки власноруч foreachкопіює елементи в новий екземпляр списку, тобто ви вирішили foreachпроблему, додавши додаткову (хоча і швидшу) foreachітерацію.
Groo

113

Коли абонент скасовує підписку, ви змінюєте вміст колекції передплатників під час перерахунку.

Існує кілька способів виправити це, один з них - це зміна циклу for на явний .ToList():

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...

64

На мою думку, більш ефективним способом є створення ще одного списку, який ви декларуєте, що ви вкладаєте все, що "потрібно видалити". Потім після закінчення основного циклу (без .ToList ()) ви робите ще один цикл над списком "для видалення", видаляючи кожен запис, як це відбувається. Тож у свій клас ви додаєте:

private List<Guid> toBeRemoved = new List<Guid>();

Потім ви зміните його на:

public void NotifySubscribers(DataRecord sr)
{
    toBeRemoved.Clear();

    ...your unchanged code skipped...

   foreach ( Guid clientId in toBeRemoved )
   {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                e.Message);
        }
   }
}

...your unchanged code skipped...

public void UnsubscribeEvent(Guid clientId)
{
    toBeRemoved.Add( clientId );
}

Це не тільки вирішить вашу проблему, але і заважатиме вам створювати список зі свого словника, що дорого, якщо там багато підписників. Якщо припустити, що список підписників, який буде видалений на будь-якій ітерації, нижчий за загальну кількість у списку, це має бути швидше. Але, звичайно, не соромтеся профайлювати це, щоб бути впевненим, що це так, якщо є сумніви у вашій конкретній ситуації використання.


9
Я очікую, що це варто розглянути, якщо у вас є робота з великими колекціями. Якби він малий, я, мабуть, просто ToList і рухатись далі.
Карл Кієнінгер

42

Ви також можете заблокувати словник підписників, щоб запобігти його зміні під час його циклу:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }

2
Це повний приклад? У мене є клас (_dictionary obj внизу), який містить загальний словник <string, int> з назвою MarkerFrequency, але це не вмить вирішило аварію: lock (_dictionary.MarkerFrequency) {foreach (KeyValuePair <string, int> пара в _dictionary.MarkerFrequency) {...}}
Джон Кумбс

4
@JCoombs можливо, ви змінюєте, можливо, перепризначаючи MarkerFrequenciesсловник всередині самого блокування, тобто початковий екземпляр більше не блокується. Спробуйте також використовувати forзамість foreach, дивіться це та це . Дайте мені знати, якщо це вирішує.
Мохаммед Сепахванд

1
Ну, нарешті, я вирішив виправити цю помилку, і ці поради були корисними - дякую! Мій набір даних був невеликим, і ця операція була лише оновленням дисплея інтерфейсу користувача, тому повторення над копією здавалося найкращим. (Я також експериментував з використанням замість foreach після блокування MarkerFrequency в обох потоках. Це запобігло збій, але, здавалося, вимагало подальшої роботи з налагодження. І це вніс більшу складність. Моя єдина причина наявності двох потоків тут - дозволити користувачу скасувати операція, до речі, користувальницький інтерфейс безпосередньо не змінює ці дані.)
Джон Кумбс

9
Проблема полягає в тому, що для великих додатків блокування може стати головним хітом продуктивності - краще використовувати колекцію в System.Collections.Concurrentпросторі імен.
BrainSlugs83

28

Чому ця помилка?

Взагалі .Net-колекції не підтримують одночасно перераховувати та змінювати. Якщо ви спробуєте змінити список колекцій під час перерахунку, він створює виняток. Таким чином, проблема, що стоїть за цією помилкою, полягає в тому, що ми не можемо змінювати список / словник, поки перебираємо те саме.

Одне з рішень

Якщо ми повторюємо словник за допомогою списку його ключів, паралельно ми можемо змінювати об’єкт словника, оскільки ми повторюємо його через колекцію ключів, а не словник (і повторюємо його колекцію ключів).

Приклад

//get key collection from dictionary into a list to loop through
List<int> keys = new List<int>(Dictionary.Keys);

// iterating key collection using a simple for-each loop
foreach (int key in keys)
{
  // Now we can perform any modification with values of the dictionary.
  Dictionary[key] = Dictionary[key] - 1;
}

Ось повідомлення в блозі про це рішення.

А для глибокого занурення в StackOverflow: Чому трапляється ця помилка?


Дякую! Чітке пояснення, чому це відбувається, і негайно дав мені спосіб вирішити це у своїй заяві.
Кім

5

Насправді проблема здається мені тим, що ви видаляєте елементи зі списку і очікуєте продовжити читання списку так, ніби нічого не сталося.

Що вам дійсно потрібно зробити - це почати від кінця і повернутися до початку. Навіть якщо ви видалите елементи зі списку, ви зможете продовжити його читання.


Я не бачу, як це могло б змінитися? Якщо ви видалите елемент у середині списку, він все одно видасть помилку, оскільки елемент, до якого він намагається отримати доступ, не може бути знайдений?
Zapnologica

4
@Zapnologica різниця полягає в тому, що - ви не перераховували б список - замість того, щоб робити для / кожного, ви б робили для / наступний і отримуєте доступ до нього цілим числом - ви можете точно змінити список у списку для / наступного циклу, але ніколи в циклі для / кожного циклу (тому що для кожного перераховується ) - ви також можете робити це вперед в / для, якщо у вас є додаткова логіка для налаштування лічильників тощо.
BrainSlugs83

4

InvalidOperationException - сталася InvalidOperationException. Він повідомляє, що "колекція була змінена" в циклі foreach

Використовуйте оператор break, як тільки об’єкт буде видалений.

колишній:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}

Гарне та просте рішення, якщо ви знаєте, що у вашому списку є щонайменше один елемент, який потрібно видалити.
Tawab Wakil

3

У мене була така ж проблема, і вона була вирішена, коли я використовував forцикл замість foreach.

// foreach (var item in itemsToBeLast)
for (int i = 0; i < itemsToBeLast.Count; i++)
{
    var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach);

   if (matchingItem != null)
   {
      itemsToBeLast.Remove(matchingItem);
      continue;
   }
   allItems.Add(itemsToBeLast[i]);// (attachDetachItem);
}

11
Цей код невірний і пропустить деякі елементи колекції, якщо будь-який елемент буде видалений. Наприклад: у вас є var arr = ["a", "b", "c"], і в першій ітерації (i = 0) ви видаляєте елемент у позиції 0 (елемент "a"). Після цього всі елементи масиву перемістяться на одну позицію вгору, а масив буде ["b", "c"]. Отже, у наступній ітерації (i = 1) ви перевірите елемент у позиції 1, який буде "c", а не "b". Це неправильно. Щоб виправити це, вам потрібно рухатись знизу вгору
Kaspars Ozols

3

Гаразд, те, що мені допомогло, було ітерацією назад. Я намагався видалити запис зі списку, але він повторювався вгору, і він вивернув цикл, оскільки запис більше не існував:

for (int x = myList.Count - 1; x > -1; x--)
                        {

                            myList.RemoveAt(x);

                        }

2

Я бачив багато варіантів цього, але для мене цей був найкращим.

ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

Потім просто прокручуйте колекцію.

Майте на увазі, що ListItemCollection може містити дублікати. За замовчуванням ніщо не заважає додавати дублікати до колекції. Щоб уникнути дублікатів, ви можете зробити це:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }

Як цей код запобігає введенню дублікатів у базу даних. Я написав щось подібне, і коли я додаю нових користувачів із списку, якщо я випадково зберігаю вибраного, який уже є у списку, він створить повторюваний запис. Чи є у вас якісь пропозиції @Mike?
Джеймі

1

Прийнята відповідь неточна і неправильна в гіршому випадку. Якщо протягом цьогоToList() року будуть внесені зміни , ви все одно можете виявити помилку. Крім того lock, які характеристики продуктивності та безпеку потоків потрібно враховувати, якщо у вас є громадський член, правильним рішенням може бути використання непорушних типів .

Взагалі незмінний тип означає, що ви не можете змінити стан його колись створеного. Отже, ваш код повинен виглядати так:

public class SubscriptionServer : ISubscriptionServer
{
    private static ImmutableDictionary<Guid, Subscriber> subscribers = ImmutableDictionary<Guid, Subscriber>.Empty;
    public void SubscribeEvent(string id)
    {
        subscribers = subscribers.Add(Guid.NewGuid(), new Subscriber());
    }
    public void NotifyEvent()
    {
        foreach(var sub in subscribers.Values)
        {
            //.....This is always safe
        }
    }
    //.........
}

Це може бути особливо корисно, якщо у вас є громадський член. Інші класи завжди можуть foreachзмінювати типи, що не змінюються, не турбуючись про зміну колекції.


1

Ось конкретний сценарій, який вимагає спеціалізованого підходу:

  1. The DictionaryЧасто перераховані.
  2. The DictionaryЗмінюється нечасто.

У цьому випадку створення копії Dictionary(або Dictionary.Values) перед кожним перерахуванням може бути досить дорогим. Моя ідея вирішити цю проблему полягає у повторному використанні однієї і тієї ж кешованої копії у кількох перерахуваннях та перегляді IEnumeratorоригіналу Dictionaryза винятками. Перечислювач буде кешований разом із скопійованими даними та допитується перед початком нового перерахування. У випадку винятку кешована копія буде відкинута, і буде створена нова. Ось моя реалізація цієї ідеї:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

public class EnumerableSnapshot<T> : IEnumerable<T>, IDisposable
{
    private IEnumerable<T> _source;
    private IEnumerator<T> _enumerator;
    private ReadOnlyCollection<T> _cached;

    public EnumerableSnapshot(IEnumerable<T> source)
    {
        _source = source ?? throw new ArgumentNullException(nameof(source));
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
        if (_enumerator == null)
        {
            _enumerator = _source.GetEnumerator();
            _cached = new ReadOnlyCollection<T>(_source.ToArray());
        }
        else
        {
            var modified = false;
            if (_source is ICollection collection) // C# 7 syntax
            {
                modified = _cached.Count != collection.Count;
            }
            if (!modified)
            {
                try
                {
                    _enumerator.MoveNext();
                }
                catch (InvalidOperationException)
                {
                    modified = true;
                }
            }
            if (modified)
            {
                _enumerator.Dispose();
                _enumerator = _source.GetEnumerator();
                _cached = new ReadOnlyCollection<T>(_source.ToArray());
            }
        }
        return _cached.GetEnumerator();
    }

    public void Dispose()
    {
        _enumerator?.Dispose();
        _enumerator = null;
        _cached = null;
        _source = null;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public static class EnumerableSnapshotExtensions
{
    public static EnumerableSnapshot<T> ToEnumerableSnapshot<T>(
        this IEnumerable<T> source) => new EnumerableSnapshot<T>(source);
}

Приклад використання:

private static IDictionary<Guid, Subscriber> _subscribers;
private static EnumerableSnapshot<Subscriber> _subscribersSnapshot;

//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();

// ...(elsewere)
foreach (var subscriber in _subscribersSnapshot)
{
    //...
}

На жаль, ця ідея наразі не може бути використана для класу Dictionaryв .NET Core 3.0, оскільки цей клас не викидає колекцію, було видозмінено виключення при перерахуванні та методах Removeі Clearвикликається. Усі інші контейнери, які я перевірив, поводяться послідовно. Я перевірив систематично ці класи: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>і SortedDictionary<T,V>. Тільки два вищезгадані способи Dictionaryкласу в .NET Core не виключають перерахування.


Оновлення: я вирішив вищезазначену проблему, порівнюючи також довжину кешованої пам’яті та оригінальну колекцію. Це виправлення припускає , що словник буде передаватися безпосередньо в якості аргументу в EnumerableSnapshotконструктор «S, і його особистість не буде прихована (наприклад) проекція , як: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Важливо: вищевказаний клас не є безпечним для потоків. Він призначений для використання з коду, що працює виключно в одному потоці.


0

Ви можете скопіювати об’єкт словника абонентів в один і той же тип об'єкта тимчасового словника, а потім повторити тимчасовий об’єкт словника за допомогою циклу foreach.


(Здається, ця публікація не дає якісної відповіді на запитання. Будь ласка, відредагуйте свою відповідь і, або просто опублікуйте її як коментар до питання).
sɐunıɔ ןɐ qɐp

0

Таким чином, іншим способом вирішення цієї проблеми було б замість видалення елементів створити новий словник і лише додати елементи, які ви не хотіли видалити, а потім замінити оригінальний словник на новий. Я не думаю, що це занадто велика проблема ефективності, оскільки це не збільшує кількість ітерацій структури.


0

Є одне посилання, де воно розроблено дуже добре і також дається рішення. Спробуйте, якщо у вас є правильне рішення, будь ласка, опублікуйте тут, щоб інші могли зрозуміти. Дане рішення нормально, як і посада, тому інші можуть спробувати ці рішення.

для вас посилання на оригінальне посилання: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Коли ми використовуємо .Net класи серіалізації для серіалізації об’єкта, де його визначення містить тип Enumerable, тобто колекцію, ви легко отримаєте InvalidOperationException, що говорить: "Колекція була модифікована; операція перерахування може не виконуватись", де ваше кодування відбувається за багатопотоковими сценаріями. Основна причина полягає в тому, що класи серіалізації будуть повторюватись через колекцію через перечислювач, тому проблема полягає в спробі ітерації колекції під час її модифікації.

По-перше, ми можемо просто використовувати замок як рішення для синхронізації, щоб гарантувати, що операція з об'єктом List може виконуватися лише з одного потоку за один раз. Очевидно, ви отримаєте штрафну ефективність, що якщо ви хочете серіалізувати колекцію цього об’єкта, то для кожного з них буде застосовано блокування.

Ну, а .Net 4.0, що робить зручні для роботи з багатопотоковими сценаріями. для цієї проблеми з серіалізаційним полем колекції я виявив, що ми можемо просто скористатися класом ConcurrentQueue (Перевірити MSDN), який є безпечним для потоків та колекцією FIFO і робить код без блокування.

Використовуючи цей клас, у своїй простоті речі, які потрібно змінити для вашого коду, замінюють тип колекції ним, використовуйте Enqueue, щоб додати елемент до кінця ConcurrentQueue, видаліть цей код блокування. Або якщо сценарій, над яким ви працюєте, вимагає збору даних, таких як List, вам знадобиться ще кілька код, щоб адаптувати ConcurrentQueue у ваші поля.

BTW, ConcurrentQueue не має чіткого методу завдяки алгоритму, що лежить в основі, який не дозволяє атомним очищенням колекції. тож вам доведеться це зробити самостійно, найшвидший спосіб - знову створити нову порожню ConcurrentQueue для заміни.


-1

Я особисто наткнувся на це нещодавно в незнайомому коді, де він знаходився у відкритому dbcontext з .qemo (елемент) Linq's. Я пекло час знаходив налагодження помилки, оскільки предмет (и) видалили вперше ітерацією, і якщо я спробував відступити назад і перейти крок назад, колекція була порожньою, оскільки я був у тому ж контексті!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.