Я не могу понять суть этой ошибки, потому что при подключении отладчика она не возникает. Ниже приведен код.
Это 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())
Если я прав, проблема исчезнет.
Вызов subscribers.Values.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
}
}
<Н3>почему эта ошибка?</Н3> В общем .Объем коллекции не поддерживают перечисляется и изменяются одновременно. Если вы пытаетесь изменять коллекцию во время перечисления, он вызывает исключение. Поэтому вопрос за эту ошибку, мы не можем изменить список/Словарь пока мы пробегаем через ту же. <Н3>одно из решений</Н3> Если мы пройдем в словарь, используя список его ключей, параллельно мы можем изменить объект словаря, как мы переборем ключи и не в словаре(и выполняя его ключей).
<Н3 и GT;пример</Н3>
//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 (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);
}
Вот конкретный сценарий, который требует специализированного подхода:
словарь
изменяется нечасто.В этом случае, создав копию словарь
(или словарь.Значения
) перед каждым перечисления могут быть весьма дорогостоящими. Мое представление о решении этой проблемы является повторное использование той же кэшированную копию в несколько перечислений, и смотреть интерфейс IEnumerator
оригинальной словарь
для исключения. Перечислитель будет кэшироваться вместе с скопированных данных, и допрашивали перед началом нового перечисления. В случае исключения кэшированную копию будут отброшены, и новый будет создан. Вот моя реализация этой идеи:
public class SafeEnumerable<T> : IEnumerable<T>, IDisposable
{
private IEnumerable<T> _source;
private IEnumerator<T> _enumerator;
private T[] _cached;
public SafeEnumerable(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 = _source.ToArray();
}
else
{
var modified = false;
if (_source is ICollection collection) // C# 7 syntax
{
modified = _cached.Length != collection.Count;
}
if (!modified)
{
try
{
_enumerator.MoveNext();
}
catch (InvalidOperationException)
{
modified = true;
}
}
if (modified)
{
_enumerator.Dispose();
_enumerator = _source.GetEnumerator();
_cached = _source.ToArray();
}
}
foreach (var item in _cached)
{
yield return item;
}
}
public void Dispose()
{
_enumerator?.Dispose();
_enumerator = null;
_cached = null;
_source = null;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public static SafeEnumerable<T> AsSafeEnumerable<T>(this IEnumerable<T> source)
=> new SafeEnumerable<T>(source);
Пример использования:
private static IDictionary<Guid, Subscriber> _subscribers;
private static SafeEnumerable<Subscriber> _safeSubscribers;
//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_safeSubscribers = _subscribers.Values.AsSafeEnumerable();
// ...(elsewere)
foreach (var subscriber in _safeSubscribers)
{
//...
}
К сожалению, эта идея не может быть в настоящее время использованы с классом словарь
в .Объем ядра 3.0, потому что этот класс не бросать сборник был изменен исключение]1, когда перечисленные и методов удалить
и ясно
вызываются. Все остальные контейнеры я проверил последовательно себя ведет. Я проверил систематически эти классы:
Список<Т>
, коллекции в<Т>
, коллекция ObservableCollection<Т>
, поиска HashSet и л;т>
, набор, в<Т>
, словарь<Т,В и GT; " и " словаре sorteddictionary<т,в>
. Только два вышеупомянутых методов словарь
класса .Объем ядра не являются недействительными перечисления.
Обновление: я исправил данную проблему, сравнивая и длины кэшируется и оригинальная коллекция. Это исправление предполагается, что словарь будет принят непосредственно в качестве аргумента SafeEnumerable
's в конструктор, и его тож не будут скрыты (например) проекция как: словарь.Выберите(е => д).AsSafeEnumerable()
.
Так что другим способом решить эту проблему будет вместо удаления элементов создать новый словарь и добавить только элементы, которые вы не хотите удалить, а затем заменить оригинальный словарь с новой. Я не'т считаю, что это слишком много, проблемы эффективности, потому что он не увеличивает количество итерации по структуре.
Есть одна ссылка, где она проработана очень хорошо & решение тоже дается. Попробуйте его, если вы получили правильное решение, пожалуйста, пост здесь, так что другие могут понять. Данное решение хорошо то как пост, так что другие могут попробовать эти решения.
по ссылке оригинал ссылке :- <а href="https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/">https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/
Когда мы используем .Net классы сериализации для сериализации объекта, в котором его определение содержится Перечислимым типом, т. е. коллекции, вы будете легко получаю исключение InvalidOperationException говорю "сбор был изменен; выполнение операции перечисления не могут", где ваши кодирования в многопоточных сценариях. Нижней причиной является то, что классы сериализации перебора коллекции с помощью перечислителя, как таковой, проблема заключается в попытке выполнить итерации через коллекцию, модифицируя его.
Первое решение, мы можем просто использовать замок в качестве решения для синхронизации, чтобы гарантировать, что операция в список объектов может быть выполнена только из одного потока одновременно. Очевидно, вы получите исполнение наказания, что если вы хотите сериализовать коллекцию этого объекта, то для каждого из них, то блокировка будет применятся.
Ну .Net версии 4.0, что делает дело с многопоточностью сценарии удобно. для этой проблемы сериализации поля коллекции, я обнаружил, что мы можем просто извлечь пользу из коллекции concurrentqueue(проверить в MSDN)класса, который является потокобезопасным и FIFO коллекции и делает блокировку кода.
Используя этот класс, в своей простоте, что вам нужно изменить в коде заменяете тип коллекции с ним, использовать помещение, чтобы добавить элемент в конец коллекции concurrentqueue, удалить код блокировки. Или, если по сценарию вы работаете, требуют материалов коллекций, таких как List, вам нужно немного больше кода, чтобы адаптироваться коллекции concurrentqueue в Ваших полях.
Кстати, коллекции concurrentqueue неâТ иметь четкий способ из-за базового алгоритма, который неâТ позволить атомарно очистки коллекции. поэтому вам придется сделать это самостоятельно, самый быстрый способ-это заново создать новый пустой коллекции concurrentqueue для замены.
Принятый ответ является неточным и неправильным в худшем случае . При внесении изменений в список()`, вы все еще можете в конечном итоге с ошибкой. Кроме "блокировка", которая по производительности и безопасности потоков должны быть приняты во внимание, если у вас есть открытый член, правильное решение можно, используя неизменяемые типы.
В общем, неизменяемый тип означает, что вы можете'т изменение состояния его когда-то создали. Так что ваш код должен выглядеть:
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
}
}
//.........
}
Это может быть особенно полезно, если у вас есть открытый член. Другие классы всегда можете объекту
на неизменяемые типы не беспокоясь о коллекции модифицируется.
Я лично недавно наткнулась на нее в незнакомом коде, где она была в открытом DbContext с помощью LINQ'ов .Удалить(элемент). У меня было адское время поиска отладки ошибку, потому что пункт(ы) были убраны в первый раз итерации, и если бы я попытался сделать шаг назад и снова шаг за коллекция была пустой, так как я был в том же контексте!