Коллекция была изменена; операция перечисления может не выполняться

737

Я не могу понять эту ошибку, потому что когда отладчик подключен, похоже, что это не происходит. Ниже приведен код.

Это сервер 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);
        }
    }
}
Теги:
dictionary
thread-safety
wcf
concurrency

13 ответов

1347
Лучший ответ

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

foreach(Subscriber s in subscribers.Values)

к

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

Если я прав, проблема исчезнет

Вызов подписчиков. Values.ToList() копирует значения подписчиков. Значения в отдельный список в начале foreach. Ничто другое не имеет доступа к этому списку (у него даже нет имени переменной!), Поэтому ничто не может изменить его внутри цикла.

  • 12
    Кстати .ToList () присутствует в dll System.Core, который не совместим с приложениями .NET 2.0. Поэтому вам может потребоваться изменить целевое приложение на .Net 3.5
  • 5
    Это замечательно. Я сделал это с ArrayList, и он также работал отлично (очевидно, с ToArray ())
Показать ещё 13 комментариев
109

Когда абонент отказывается от подписки, вы меняете содержимое коллекции подписчиков во время перечисления.

Есть несколько способов исправить это, один из которых меняет цикл for, чтобы использовать явный .ToList():

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

Более эффективный способ, на мой взгляд, состоит в том, чтобы иметь еще один список, который вы заявляете, что вы помещаете все, что нужно "удалить". Затем, после завершения основного цикла (без .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 );
}

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

  • 5
    Я ожидаю, что это стоит рассмотреть, если у вас есть, вы работаете с большими коллекциями. Если бы он был маленьким, я бы, вероятно, просто ToList и двигаться дальше.
34

Вы также можете заблокировать словарь подписчиков, чтобы он не изменялся всякий раз, когда он был зациклен:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
  • 6
    +1 за цитирование основного метода многопоточного программирования
  • 2
    Это полный пример? У меня есть класс (_dictionary obj ниже), который содержит общий словарь <string, int> с именем MarkerFrequencies, но это не сразу решает проблему сбоя: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> пара в _dictionary.MarkerFrequencies) {...}}
Показать ещё 3 комментария
11

Примечание. В целом коллекции .Net не поддерживают перечисление и изменение в одно и то же время. Если вы попытаетесь изменить список коллекции, когда находитесь в середине перечисления, это вызовет исключение.

Итак, проблема этой ошибки заключается в том, что мы не можем изменять список/словарь во время цикла. Но если мы итерируем словарь, используя временный список его ключей, мы можем изменить объект словаря, потому что теперь мы не итерируем словарь (и итерируем его коллекцию ключей).

:

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

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

Вот сообщение об этом решении.

И для глубокого погружения в stackoverflow: Почему эта ошибка возникает?

  • 1
    Абсолютно фантастическое объяснение.
  • 0
    Согласовано! Спасибо, что объяснили, что произошло и ПОЧЕМУ !! Это было так полезно.
4

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

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

  • 0
    Я не понимаю, как это будет иметь значение? Если вы удалите элемент в середине списка, он все равно выдаст ошибку, так как элемент, к которому он пытается получить доступ, не найден?
  • 4
    @Zapnologica разница в том, что вы не будете перечислять список - вместо того, чтобы делать для / каждого, вы будете делать для / следующего и получить к нему доступ с помощью целого числа - вы определенно можете изменить список а в цикл for / next, но никогда не в цикле for / each (потому что для перечисления for / each) - вы также можете делать это вперед в цикле for / next, если у вас есть дополнительная логика для настройки счетчиков и т. д.
3

InvalidOperationException - Исключено InvalidOperationException. Он сообщает, что "коллекция была изменена" в foreach-loop

Используйте оператор break, как только объект будет удален.

ex:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
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);
            }
  • 0
    Как этот код предотвратит ввод дубликатов в базу данных? Я написал что-то подобное, и когда я добавляю новых пользователей из списка, если я случайно оставлю одного выбранного, который уже есть в списке, он создаст дублирующую запись. У вас есть предложения @Mike?
  • 0
    @jlg Смотрите мой обновленный ответ выше.
2

У меня была та же проблема, и она была решена, когда я использовал цикл 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);
}
  • 10
    Этот код неверен и пропустит некоторые элементы в коллекции, если какой-либо элемент будет удален. Например: у вас есть var arr = ["a", "b", "c"] и в первой итерации (i = 0) вы удаляете элемент в позиции 0 (элемент "a"). После этого все элементы массива переместятся на одну позицию вверх, и массив будет ["b", "c"]. Итак, на следующей итерации (i = 1) вы будете проверять элемент в позиции 1, который будет «c», а не «b». Это не верно. Чтобы это исправить, вы должны двигаться снизу вверх
  • 0
    Выше код не работает со списками.
1

Ладно, так что помогло мне повторить назад. Я пытался удалить запись из списка, но итерации вверх, и это закрутило цикл, потому что запись больше не существовала:

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

                            myList.RemoveAt(x);

                        }
0

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

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

Когда мы используем классы.Net Serialization для сериализации объекта, где его определение содержит тип Enumerable, то есть коллекцию, вы легко получите InvalidOperationException, говорящее "Коллекция была изменена; операция перечисления может не выполняться", если кодирование выполняется в многопоточных сценариях. Основная причина в том, что классы сериализации будут перебирать коллекцию через перечислитель, поэтому проблема заключается в попытке перебрать коллекцию при ее изменении.

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

Ну,.Net 4.0, который делает работу с многопоточными сценариями удобной. Я обнаружил, что для этой проблемы с полями сериализации Collection мы можем воспользоваться классом ConcurrentQueue (Check MSDN), который является поточно-ориентированным и FIFO-коллекцией и делает код без блокировки.

Используя этот класс, в его простоте материал, который вам нужно изменить для своего кода, заменяет тип Collection на него, используйте Enqueue, чтобы добавить элемент в конец ConcurrentQueue, удалите этот код блокировки. Или, если сценарий, над которым вы работаете, требует таких сборщиков, как List, вам потребуется еще немного кода, чтобы адаптировать ConcurrentQueue к вашим полям.

Кстати, ConcurrentQueue не имеет метода Clear из-за базового алгоритма, который не допускает атомарной очистки коллекции. так что вы должны сделать это сами, самый быстрый способ - создать новое пустое ConcurrentQueue для замены.

0

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

0

Вы можете скопировать объект словаря подписчиков на объект временного словаря того же типа, а затем перебрать объект временного словаря с помощью цикла foreach.

  • 0
    (Похоже, что это сообщение не дает качественного ответа на вопрос. Пожалуйста, либо отредактируйте свой ответ и, либо просто опубликуйте его как комментарий к вопросу).

Ещё вопросы

Сообщество Overcoder
Наверх
Меню