1068

我无法深入了解这个错误,因为在附加调试器时,它似乎没有发生。

收藏已修改;枚举操作可能无法执行

下面是代码。

这是 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);
        }
    }
}
4

16 回答 16

1889

可能发生的情况是,SignalData在循环过程中间接更改了引擎盖下的订阅者字典并导致该消息。您可以通过更改来验证这一点

foreach(Subscriber s in subscribers.Values)

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

如果我是对的,问题就会消失。

调用subscribers.Values.ToList()将 的值复制subscribers.Values到 开头的单独列表中foreach。没有其他东西可以访问这个列表(它甚至没有变量名!),所以没有任何东西可以在循环内修改它。

于 2009-03-03T02:10:17.173 回答
124

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

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
于 2009-03-03T02:13:12.563 回答
77

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

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

于 2009-03-03T06:58:46.510 回答
44

为什么会出现这个错误?

一般来说,.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:为什么会出现此错误?

于 2014-11-11T12:12:12.777 回答
10

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

for (int x = myList.Count - 1; x > -1; x--)
{
    myList.RemoveAt(x);
}
于 2018-07-22T14:14:46.080 回答
6

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

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

前任:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
于 2017-03-16T07:20:55.977 回答
5

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

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

于 2012-05-23T16:10:21.383 回答
5

在最坏的情况下,接受的答案是不精确和不正确的。如果在 期间进行了更改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使用不可变类型,而不必担心集合被修改。

于 2019-09-05T06:26:40.227 回答
3

我想指出任何答案中都没有反映的其他情况。我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>,在离开受锁保护的部分后仍然没有枚举。因此,它可以在被枚举之前被其他线程更改,从而导致异常。解决方案是强制枚举,例如.ToList()注释中所示。这样,可枚举的对象在离开受保护部分之前就已经被枚举了。

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

于 2021-01-27T13:50:32.380 回答
2

我有同样的问题,当我使用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);
}
于 2014-06-16T07:29:50.843 回答
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);
            }
于 2015-03-04T20:55:23.760 回答
2

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

 while (list.Count > 0)
 {
    string Item = list[0];
    list.RemoveAt(0);
 
    // do here what you need to do with item
 
 } 
 

如果函数在仍在执行时被调用,项目将不会从第一次重复,因为它们一旦被使用就会被删除。对于小型列表,应该不会对性能产生太大影响。

于 2020-11-12T10:58:40.020 回答
1

有一个链接,它阐述得非常好,还给出了解决方案。如果您有正确的解决方案,请尝试在此处发布,以便其他人可以理解。给定的解决方案是好的,然后就像帖子一样,所以其他人可以尝试这些解决方案。

供您参考原始链接:- 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 进行替换。

于 2018-12-13T05:33:59.970 回答
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 Core 3.0 中的类一起使用,因为这个类在枚举和调用方法时不会抛出Collection was modified异常。我检查的所有其他容器都表现一致。我系统地检查了这些类: 、、、、和。只有.NET Core 中类的上述两个方法不会使枚举无效。RemoveClearList<T>Collection<T>ObservableCollection<T>HashSet<T>SortedSet<T>Dictionary<T,V>SortedDictionary<T,V>Dictionary


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


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

于 2019-10-30T04:52:41.343 回答
0

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

于 2013-05-29T13:59:41.383 回答
0

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

于 2013-10-04T14:04:10.027 回答