集合已修改;枚举操作可能无法执行

我无法了解此错误的底部,因为当调试器附加时,它似乎不会发生。

集合已修改;枚举操作可能无法执行

下面是代码。

这是Windows服务中的WCF服务器。每当有数据事件时,服务都会调用方法NotifySubscribers()(随机间隔,但不是很频繁-每天大约800次)。

当Windows窗体客户端订阅时,订阅者ID会添加到订阅者字典中,当客户端取消订阅时,它会从字典中删除。错误发生在客户端取消订阅时(或之后)。似乎下次调用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);}}}
898582 次浏览

可能发生的情况是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 );}

这不仅可以解决你的问题,还可以防止你不得不继续从字典中创建列表,如果那里有很多订阅者,这会很昂贵。假设在任何给定迭代中要删除的订阅者列表低于列表中的总数,这应该会更快。但当然,如果对你的特定使用情况有任何疑问,请随时对其进行分析,以确保是这种情况。

实际上,在我看来,问题是您正在从列表中删除元素,并期望继续阅读列表,就好像什么都没发生一样。

你真正需要做的是从结尾开始,回到开头。即使你从列表中删除元素,你也可以继续阅读它。

您可以将订阅者字典对象复制到相同类型的临时字典对象,然后使用foreach循环迭代临时字典对象。

所以解决这个问题的另一种方法是不是删除元素创建一个新字典,只添加你不想删除的元素,然后用新字典替换原始字典。我不认为这是一个太大的效率问题,因为它不会增加你迭代结构的次数。

我也有同样的问题,当我使用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);}

为什么这个错误?

一般。网络集合不支持同时枚举和修改。如果您尝试在枚举过程中修改集合列表,会引发异常。所以这个错误背后的问题是,我们在循环遍历列表/字典时无法修改列表/字典。

解决方案之一

如果我们使用键列表迭代字典,我们可以并行地修改字典对象,因为我们正在迭代键集合和不是字典(并迭代其键集合)。

示例

//get key collection from dictionary into a list to loop throughList<int> keys = new List<int>(Dictionary.Keys);
// iterating key collection using a simple for-each loopforeach (int key in keys){// Now we can perform any modification with values of the dictionary.Dictionary[key] = Dictionary[key] - 1;}

这里有一个关于这个解决方案的博客文章

对于StackOverflow的深入研究:为什么会出现这个错误?

我见过很多选择,但对我来说,这是最好的。

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

无效的操作异常-发生了一个InvalidOperationException。它报告一个“集合被修改”在一个Foreach循环中

一旦对象被删除,请使用brex语句。

例如:

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

好的,所以帮助我的是向后迭代。我试图从列表中删除一个条目,但向上迭代,它搞砸了循环,因为该条目不再存在:

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

有一个环节,它阐述得很好,也给出了解决方案。 试试看,如果你有适当的解决方案,请在这里张贴,以便其他人可以理解。 给出的解决方案是可以的,然后喜欢的职位,所以其他人可以尝试这些解决方案。

原始链接:- Https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

当我们使用.Net Serialization 类序列化其定义包含枚举类型的对象时,即。 集合,您将很容易得到 InvalidOperationException 说: “集合被修改; “枚举操作可能无法执行”,而您的编码是在多线程方案下进行的。 根本原因是序列化类将通过枚举器迭代整个集合, 问题在于在修改集合时尝试迭代该集合。

第一个解决方案,我们可以简单地使用锁作为同步解决方案,以确保 对 List 对象的操作一次只能从一个线程执行。 显然,你会得到性能惩罚 如果您想序列化该对象的集合,那么对于每个对象,都将应用锁。

好吧,. Net 4.0使得处理多线程场景变得很方便。 对于这个序列化 Collection 字段问题,我发现我们可以从 ConcurrentQueue (Check MSDN)类中获益, 这是一个线程安全和 FIFO 集合,使代码无锁。

使用这个简单的类,您需要为代码修改的内容就是用它替换 Collection 类型, 使用 Enqueue 向 ConcurrentQueue 的末尾添加一个元素,删除这些锁代码。 或者,如果您正在处理的场景确实需要诸如 List 之类的收集工具,那么您将需要更多的代码来将 ConcurrentQueue 适应到您的字段中。

顺便说一句,由于底层算法不允许自动清除集合,因此 ConcurrentQueue 没有 Clear 方法。 所以你必须自己做,最快的方法是重新创建一个新的空 ConcurrentQueue 来替换。

在最坏的情况下,公认的答案是不准确和不正确的。如果在 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,而不必担心集合被修改。

以下是需要采取专门办法的具体情况:

  1. Dictionary经常被枚举。
  2. Dictionary很少被修改。

在这种情况下,在每次枚举之前创建一个 Dictionary(或 Dictionary.Values)的副本会非常昂贵。我解决这个问题的想法是在多个枚举中重用相同的缓存副本,并观察原始 DictionaryIEnumerator中的异常。枚举数将与复制的数据一起缓存,并在启动新枚举之前进行询问。如果出现异常,缓存的副本将被丢弃,并创建一个新的副本。下面是我对这个想法的实现:

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,因为在枚举时会调用 这个类不会抛出一个 < em > Collection was Amendment 异常以及方法 RemoveClear。我检查过的所有其他集装箱都没有问题。我系统地检查了这些课程: List<T>Collection<T>ObservableCollection<T>HashSet<T>SortedSet<T>Dictionary<T,V>SortedDictionary<T,V>。中的 Dictionary类的上述两个方法。NET 核心不会使枚举无效。


更新: 我通过比较缓存和原始集合的长度修复了上面的问题。此修复程序假定字典将作为参数直接传递给 EnumerableSnapshot的构造函数,并且它的标识将不会被(例如)类似于 dictionary.Select(e => e).ΤοEnumerableSnapshot()的投影所隐藏。


重要提示: 以上类是 没有线程安全的。它旨在从专门在单个线程中运行的代码中使用。

这种方法应该涵盖在函数仍在执行时再次调用函数的情况(并且项只需要使用一次) :

 while (list.Count > 0)
{
string Item = list[0];
list.RemoveAt(0);
 

// do here what you need to do with item
 

}
 

如果函数在仍然执行时被调用,那么第一个项将不会再次重复,因为它们一旦被使用就会被删除。 不应该对小列表的性能造成太大影响。

我想指出其他没有反映在任何答案中的情况。我有一个 Dictionary<Tkey,TValue>共享在一个多线程应用程序,它使用一个 ReaderWriterLockSlim来保护读写操作。这是一个引发异常的阅读方法:

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 的一个微妙之处: 这段代码返回一个 IEnumerable<Info>,在保留受锁保护的部分之后仍然没有枚举这个 IEnumerable<Info>。因此,在枚举之前,其他线程可以对其进行更改,从而导致异常。解决方案是强制枚举,例如使用注释中显示的 .ToList()。通过这种方式,可枚举数在离开受保护部分之前就已经被枚举了。

因此,如果在多线程应用程序中使用 LINQ,请注意在离开受保护区域之前始终具体化查询。