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

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

Это сервер 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);
        }
    }
}
737
03 марта '09 в 5:01
источник поделиться
13 ответов

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

foreach(Subscriber s in subscribers.Values)

к

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

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

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

1347
03 марта '09 в 5:10
источник

Связанные вопросы


Похожие вопросы

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

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
104
03 марта '09 в 5:13
источник

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

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

55
03 марта '09 в 9:58
источник

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

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
35
23 мая '12 в 22:32
источник

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

11
11 нояб. '14 в 15:12
источник

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

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

4
23 мая '12 в 19:10
источник

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

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

ex:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
3
16 марта '17 в 10:20
источник

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

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

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);
            }
2
04 марта '15 в 23:55
источник

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

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

                            myList.RemoveAt(x);

                        }
1
22 июля '18 в 17:14
источник

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

0
29 мая '13 в 16:59
источник

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

0
04 окт. '13 в 17:04
источник

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

для вас ссылка на оригинальную ссылку: - 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
13 дек. '18 в 8:33
источник

Посмотрите другие вопросы по меткам или Задайте вопрос