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

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

Это сервер 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);
        }
    }
}

Ответ 1

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

foreach(Subscriber s in subscribers.Values)

к

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

Почему эта ошибка?

В целом коллекции .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;
}

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

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

Ответ 6

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

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

Ответ 7

У меня была та же проблема, и она была решена, когда я использовал цикл 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);
}

Ответ 8

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

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

ex:

ArrayList list = new ArrayList(); 

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

Ответ 9

Я видел много вариантов для этого, но для меня этот был лучшим.

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

Ответ 10

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

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

                            myList.RemoveAt(x);

                        }

Ответ 11

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

Ответ 12

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

Ответ 13

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

для вас ссылка на оригинальную ссылку: - 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 для замены.

Ответ 14

Принятый ответ неточен и неверен в худшем случае. Если изменения сделаны во время 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 на неизменяемых типах, не беспокоясь об изменении коллекции.