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


908

我无法弄清此错误的原因,因为在附加调试器后,似乎没有发生此错误。下面是代码。

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

Windows Forms客户端进行预订时,订户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);
        }
    }
}

在我的情况下,这是附带作用,因为我使用了一些在过程中被修改的.include(“ table”)-在阅读代码时不是很明显。但是我很幸运不需要那些包含(是的,旧的,无需维护的代码),我通过删除它们解决了我的问题
Adi

Answers:


1630

可能发生的情况是,SignalData在循环期间间接更改了后台的订户字典并导致该消息。您可以通过更改

foreach(Subscriber s in subscribers.Values)

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

如果我说对了,问题就会消失

呼叫subscribers.Values.ToList()将的值复制subscribers.Values到的开头foreach。没有其他人可以访问此列表(它甚至没有变量名!),因此没有什么可以在循环内对其进行修改。


14
BTW .ToList()存在于System.Core dll中,它与.NET 2.0应用程序不兼容。因此,您可能需要将目标应用程序更改为.Net 3.5
mishal153 2010年

60
我不明白您为什么要执行ToList以及为什么可以解决所有问题
PositiveGuy

204
@CoffeeAddict:问题是subscribers.Values正在foreach循环内进行修改。呼叫subscribers.Values.ToList()将的值复制subscribers.Values到的开头foreach。没有其他人可以访问此列表(它甚至没有变量名!),因此没有什么可以在循环内对其进行修改。
BlueRaja-Danny Pflughoeft

31
请注意,ToList如果在ToList执行过程中修改了集合,也可能会抛出该异常。
Sriram Sakthivel

16
我敢肯定,这不能解决问题,而只是使其难以复制。ToList不是原子操作。更有趣的是,ToList基本上是在foreach内部进行操作以将项目复制到新的列表实例中,这意味着您可以foreach通过添加其他(虽然更快)foreach迭代来解决问题。
Groo 2015年

113

订阅者取消订阅时,您将在枚举期间更改订阅者集合的内容。

有几种方法可以解决此问题,一种方法是将for循环更改为使用显式的.ToList()

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

64

在我看来,一种更有效的方法是拥有另一个列表,声明您将“要删除的任何内容”放入其中。然后,在完成主循环(不带.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 );
}

这不仅可以解决您的问题,而且可以防止您必须继续从字典中创建列表,如果其中有很多订阅者,这将是昂贵的。假设在任何给定的迭代中要删除的订户列表低于列表中的总数,则此列表应该更快。但是,当然可以对其进行概要分析,以确保在您的特定使用情况有任何疑问时也是如此。


9
我希望这是值得考虑的,如果您有的话,那么您正在处理更大的收藏。如果它很小,我可能只是ToList继续前进。
Karl Kieninger 2015年

42

您还可以锁定您的订户字典,以防止其在循环时被修改:

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

2
那是一个完整的例子吗?我有一个类(下面的_dictionary obj)包含一个名为MarkerFrequencies的泛型Dictionary <string,int>,但是这样做并不能立即解决崩溃问题:lock(_dictionary.MarkerFrequencies){foreach(KeyValuePair <string,int> pair in _dictionary.MarkerFrequency){...}}
乔恩·库姆斯

4
@JCoombs可能正在修改,可能MarkerFrequencies在锁本身内部重新分配了字典,这意味着原始实例不再处于锁定状态。也可以尝试使用for,而不是一个foreach,见这个这个。让我知道是否可以解决。
Mohammad Sepahvand

1
好吧,我终于可以解决这个错误了,这些技巧很有帮助-谢谢!我的数据集很小,此操作只是UI显示更新,因此遍历副本似乎是最好的。(我还尝试在两个线程中锁定MarkerFrequency之后使用for而不是foreach。这可以防止崩溃,但似乎需要进一步的调试工作。这引入了更多的复杂性。在这里拥有两个线程的唯一原因是使用户可以取消顺便说一句,用户界面不会直接修改该数据。)
Jon Coombs 2014年

9
问题是,对于大型应用程序,锁可能会严重影响性能-最好在System.Collections.Concurrent名称空间中使用集合。
BrainSlugs83 2014年

28

为什么会出现这个错误?

通常,.Net集合不支持同时枚举和修改。如果尝试在枚举期间修改集合列表,则会引发异常。因此,此错误背后的问题是,当我们遍历列表/字典时,我们无法对其进行修改。

解决方案之一

如果我们使用其键列表来迭代字典,则可以并行地修改字典对象,因为我们要遍历键集合而不是字典(并遍历其键集合)。

//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:为什么会发生此错误?


谢谢!清楚地解释了为什么会发生这种情况,并立即为我提供了在应用程序中解决此问题的方法。
金·克罗斯

5

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

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


我看不出会有什么不同?如果删除列表中间的元素,它仍然会引发错误,因为找不到试图访问的项目?
Zapnologica

4
@Zapnologica的区别是-您不会枚举列表-而不是执行for / each,而是执行for / next并按整数访问它-您绝对可以在列表中修改列表a用于/下一个循环,但从来没有在对/每个回路(因为/每个枚举) -你也可以做一个对/下一向前去,只要你有额外的逻辑来调整计数器等
BrainSlugs83

4

InvalidOperationException- 发生InvalidOperationException。它在foreach循环中报告“集合已被修改”

使用break语句,一旦删除对象。

例如:

ArrayList list = new ArrayList(); 

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

如果您知道您的列表中最多有一项需要删除,那么这是一种好简单的解决方案。
Tawab Wakil

3

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

11
此代码是错误的,如果要删除任何元素,将跳过集合中的某些项目。例如:您具有var arr = [“ a”,“ b”,“ c”],并且在第一次迭代(i = 0)中,删除了位置0处的元素(元素“ a”)。此后,所有数组元素将上移一个位置,数组将为[“ b”,“ c”]。因此,在下一次迭代(i = 1)中,您将检查位置1处的元素,该元素将为“ c”而不是“ b”。错了 要解决此问题,您必须从底部移至顶部
Kaspars Ozols

3

好的,所以帮助我的是向后迭代。我试图从列表中删除一个条目,但向上进行迭代,并且由于该条目不再存在而使循环搞砸了:

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

                            myList.RemoveAt(x);

                        }

2

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

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

此代码将如何防止重复项输入数据库。我写了类似的文章,当我从列表框中添加新用户时,如果我不小心将一个选定的用户保留在列表中,它将创建一个重复的条目。您对@Mike有什么建议吗?
杰米

1

在最坏的情况下,可接受的答案是不准确且不正确的。如果在期间进行了更改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

这是一个需要采取特殊方法的特定方案:

  1. Dictionary经常列举。
  2. 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 3.0的核心,因为此类不抛出一个收集修改异常列举时与方法RemoveClear被调用。我检查过的所有其他容器都表现一致。我检查系统这些类: List<T>Collection<T>ObservableCollection<T>HashSet<T>SortedSet<T>Dictionary<T,V>SortedDictionary<T,V>Dictionary.NET Core中只有上述两个类的方法不会使枚举无效。


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


重要:上面的类不是线程安全的。它旨在从仅在单个线程中运行的代码中使用。


0

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


(此帖子似乎无法为问题提供高质量的答案。请编辑您的答案,或者只是将其作为对问题的评论发表)。
sɐunıɔןɐqɐp

0

因此,解决此问题的另一种方法是,不删除元素而创建新字典,而仅添加您不想删除的元素,然后用新字典替换原始字典。我认为这并不是效率问题,因为它不会增加您遍历结构的次数。


0

它很好地阐述了一个环节,并给出了解决方案。如果您有适当的解决方案,请尝试一下,请在此处发布,以便其他人可以理解。给定解决方案就可以了,就像帖子一样,因此其他人可以尝试这些解决方案。

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

当我们使用.Net序列化类来序列化其定义包含Enumerable类型(即集合)的对象时,如果您的编码是在多线程方案下进行的,则您将很容易得到InvalidOperationException,其中说“集合已被修改;枚举操作可能无法执行”。根本原因是序列化类将通过枚举器遍历集合,因此,问题在于尝试在修改集合时遍历集合。

第一种解决方案,我们可以简单地将锁用作同步解决方案,以确保对List对象的操作一次只能从一个线程执行。显然,您将获得性能损失,如果您要序列化该对象的集合,则将对每个对象应用锁。

好了,.Net 4.0使得处理多线程方案变得很方便。对于此序列化Collection字段问题,我发现我们可以从ConcurrentQueue(Check MSDN)类中受益,该类是线程安全的FIFO集合,并且使代码无锁。

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

顺便说一句,由于基础算法不允许原子清除集合,因此ConcurrentQueue没有Clear方法。因此,您必须自己做,最快的方法是重新创建一个新的空ConcurrentQueue进行替换。


-1

我最近在一个不熟悉的代码中亲自遇到了这个问题,它与Linq的.Remove(item)在打开的dbcontext中。我很费时间查找错误调试信息,因为项在第一次迭代时就被删除了,而且如果我试图退后一步,因为我是在同一上下文中,那么该集合是空的!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.