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

StackOverflow https://stackoverflow.com/questions/604831

Вопрос

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

Это сервер 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);
        }
    }
}
Это было полезно?

Решение

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

foreach(Subscriber s in subscribers.Values)

Для

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

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

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

Другие советы

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

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

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

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

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

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

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

Почему возникла эта ошибка?

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

Одно из решений

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

Пример

//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: Почему возникает эта ошибка?

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

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

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

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

бывший:

ArrayList list = new ArrayList(); 

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

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

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

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

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

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

                            myList.RemoveAt(x);

                        }

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

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

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

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

Когда мы используем .Сетевые классы сериализации для сериализации объекта, в определении которого содержится перечислимый тип, т.е.collection, вы легко получите InvalidOperationException с сообщением "Коллекция была изменена;операция перечисления может не выполняться", если ваше кодирование выполняется в многопоточных сценариях.Основная причина заключается в том, что классы сериализации будут выполнять итерацию по коллекции через enumerator, как таковой, проблема заключается в попытке выполнить итерацию по коллекции при ее изменении.

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top