我无法找到这个错误的根源,因为当附加调试器时,它似乎没有发生。

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

下面是代码。

这是Windows服务中的WCF服务器。只要有数据事件,服务就会调用NotifySubscribers()方法(随机间隔,但不经常——大约每天800次)。

When a Windows Forms client subscribes, the subscriber ID is added to the subscribers dictionary, and when the client unsubscribes, it is deleted from the dictionary. The error happens when (or after) a client unsubscribes. It appears that the next time the NotifySubscribers() method is called, the foreach() loop fails with the error in the subject line. The method writes the error into the application log as shown in the code below. When a debugger is attached and a client unsubscribes, the code executes fine.

您认为这段代码有问题吗?我需要使字典线程安全吗?

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

当前回答

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

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

ex:

ArrayList list = new ArrayList(); 

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

其他回答

在我看来,一个更有效的方法是另一个列表,你声明你把任何“要删除”的东西都放进去。然后,在完成主循环之后(不使用. 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 );
}

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

其中有一个环节阐述得很好,并给出了解决方案。 尝试一下,如果你有合适的解决方案,请张贴在这里,这样其他人就能理解。 给出的解决方案是可以的,然后像帖子一样,所以其他人可以尝试这些解决方案。

供您参考原始链接:- 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使处理多线程场景变得很方便。 对于这个序列化收集字段的问题,我发现我们可以从ConcurrentQueue(检查MSDN)类中获益, 这是一个线程安全的FIFO集合,并使代码无锁。

使用这个类,在它的简单性中,你需要为你的代码修改的东西是用它替换Collection类型, 使用Enqueue添加一个元素到ConcurrentQueue的末尾,删除那些锁代码。 或者,如果您正在处理的场景确实需要像List这样的集合,那么您将需要更多的代码来将ConcurrentQueue调整到字段中。

顺便说一句,ConcurrentQueue doesnât有一个清除方法,因为底层算法doesnât允许原子地清除集合。 所以你必须自己做,最快的方法是重新创建一个新的空的ConcurrentQueue来替换。

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

下面是一个特定的场景,需要使用专门的方法:

字典经常被列举出来。 字典很少被修改。

In this scenario creating a copy of the Dictionary (or the Dictionary.Values) before every enumeration can be quite costly. My idea about solving this problem is to reuse the same cached copy in multiple enumerations, and watch an IEnumerator of the original Dictionary for exceptions. The enumerator will be cached along with the copied data, and interrogated before starting a new enumeration. In case of an exception the cached copy will be discarded, and a new one will be created. Here is my implementation of this idea:

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)
{
    //...
}

Unfortunately this idea cannot be used currently with the class Dictionary in .NET Core 3.0, because this class does not throw a Collection was modified exception when enumerated and the methods Remove and Clear are invoked. All other containers I checked are behaving consistently. I checked systematically these classes: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V> and SortedDictionary<T,V>. Only the two aforementioned methods of the Dictionary class in .NET Core are not invalidating the enumeration.


更新:通过比较缓存和原始集合的长度,我修复了上述问题。这个修正假设字典将直接作为参数传递给EnumerableSnapshot的构造函数,并且它的标识不会被(例如)像:dictionary这样的投影隐藏。选择(e => e).ΤοEnumerableSnapshot()。


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

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

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

ex:

ArrayList list = new ArrayList(); 

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