Коллекция была изменена; операция перечисления может не выполняться
Я не могу докопаться до этой ошибки, потому что, когда отладчик подключен, это не происходит. Ниже приведен код.
Это сервер WCF в службе Windows. Метод NotifySubscribeers вызывается службой всякий раз, когда происходит событие данных (через случайные интервалы, но не очень часто - около 800 раз в день).
Когда клиент Windows Forms подписывается, идентификатор подписчика добавляется в словарь подписчиков, а когда клиент отписывается, он удаляется из словаря. Ошибка происходит, когда (или после) клиент отписывается. Похоже, что при следующем вызове метода NotifySubscribeers() цикл 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);
}
}
}
17 ответов
Вероятно, происходит то, что SignalData косвенно изменяет словарь подписчиков под капотом во время цикла и приводит к этому сообщению. Вы можете проверить это, изменив
foreach(Subscriber s in subscribers.Values)
к
foreach(Subscriber s in subscribers.Values.ToList())
Если я прав, проблема исчезнет
Вызов подписчиков.Values.ToList() копирует значения подписчиков. Значения в отдельный список в начале 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 );
}
Это не только решит вашу проблему, но и избавит вас от необходимости создавать список из вашего словаря, что дорого, если там много подписчиков. Предполагая, что список подписчиков, которые будут удалены на любой итерации, меньше, чем общее число в списке, это должно быть быстрее. Но, конечно, не стесняйтесь профилировать его, чтобы быть уверенным, что это так, если есть какие-либо сомнения в вашей конкретной ситуации использования.
Примечание. В целом коллекции.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;
}
Вот сообщение в блоге об этом решении.
И для глубокого погружения в stackru: почему возникает эта ошибка?
Вы также можете заблокировать словарь подписчиков, чтобы предотвратить его изменение при каждом зацикливании:
lock (subscribers)
{
foreach (var subscriber in subscribers)
{
//do something
}
}
Принятый ответ неточен и в худшем случае неверен. Если изменения внесены во время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
на неизменяемые типы, не беспокоясь об изменении коллекции.
Итак, что помогло мне, итерация назад. Я пытался удалить запись из списка, но делал итерации вверх, и это запутало цикл, потому что запись больше не существует:
for (int x = myList.Count - 1; x > -1; x--)
{
myList.RemoveAt(x);
}
Хочу указать на другой случай, не отраженный ни в одном из ответов. у меня есть
public IEnumerable<Data> GetInfo()
{
List<Data> info = null;
_cacheLock.EnterReadLock();
try
{
info = _cache.Values.SelectMany(ce => ce.Data); // Ad .Tolist() to avoid exc.
}
finally
{
_cacheLock.ExitReadLock();
}
return info;
}
В целом работает нормально, но время от времени получаются исключения. Проблема заключается в тонкости LINQ: этот код возвращает
Итак, если вы используете LINQ в многопоточном приложении, не забывайте всегда материализовать запросы перед тем, как покинуть защищенные области.
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);
}
Этот способ должен охватывать ситуацию параллелизма, когда функция вызывается снова, пока все еще выполняется (а элементы необходимо использовать только один раз):
while (list.Count > 0)
{
string Item = list[0];
list.RemoveAt(0);
// do here what you need to do with item
}
Если функция вызывается во время выполнения, элементы не будут повторяться с первого раза, поскольку они будут удалены, как только они будут использованы. Не должно сильно влиять на производительность для небольших списков.
Есть одна ссылка, где она разработана очень хорошо, и решение также дается. Попробуйте, если у вас есть правильное решение, пожалуйста, напишите здесь, чтобы другие могли понять. Данное решение в порядке, тогда как пост, так что другие могут попробовать это решение.
для вас ссылка на оригинальную ссылку: - 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 для замены.
Вот конкретный сценарий, требующий особого подхода:
- В
Dictionary
часто перечисляется. - В
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, поскольку этот класс не генерирует исключение Collection was modified при перечислении, а методы Remove
а также Clear
вызываются. Все остальные контейнеры, которые я проверял, работают стабильно. Я систематически проверял эти классы:List<T>
, Collection<T>
, ObservableCollection<T>
, HashSet<T>
, SortedSet<T>
, Dictionary<T,V>
а также SortedDictionary<T,V>
. Только два вышеупомянутых методаDictionary
class в.NET Core не аннулируют перечисление.
Обновление: я исправил указанную выше проблему, сравнив также длину кэшированной и исходной коллекции. Это исправление предполагает, что словарь будет передан напрямую как аргумент вEnumerableSnapshot
конструктора, и его идентификатор не будет скрыт (например) такой проекцией, как: dictionary.Select(e => e).ΤοEnumerableSnapshot()
.
Важно: указанный выше класс не является потокобезопасным. Он предназначен для использования из кода, работающего исключительно в одном потоке.
Вы можете скопировать объект словаря подписчиков во временный объект словаря того же типа, а затем выполнить итерацию объекта временного словаря, используя цикл foreach.
Таким образом, другим способом решения этой проблемы было бы вместо удаления элементов создать новый словарь и добавить только те элементы, которые вы не хотите удалять, а затем заменить исходный словарь новым. Я не думаю, что это слишком большая проблема эффективности, потому что это не увеличивает количество итераций по структуре.