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

时间:2009-03-03 02:01:59

标签: c# wcf concurrency dictionary thread-safety

我无法理解这个错误的底部,因为当附加调试器时,它似乎不会发生。以下是代码。

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

当Windows窗体客户端订阅时,订阅者ID将添加到订阅者字典中,当客户端取消订阅时,将从字典中删除该订阅者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);
        }
    }
}

18 个答案:

答案 0 :(得分:1438)

可能发生的事情是SignalData在循环期间间接改变了引擎盖下的订阅者字典并导致该消息。您可以通过更改

来验证这一点
foreach(Subscriber s in subscribers.Values)

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

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

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

答案 1 :(得分:107)

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

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

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

答案 2 :(得分:58)

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

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

答案 3 :(得分:40)

您还可以锁定订阅者字典,以防止它在循环时被修改:

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

答案 4 :(得分:17)

为什么会出现此错误?

通常,.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;
}

这是关于此解决方案的blog post

深入了解StackOverflow:Why this error occurs?

答案 5 :(得分:5)

实际上,在我看来,问题是您要从列表中删除元素并期望继续读取列表,就像没有发生任何事情一样。

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

答案 6 :(得分:3)

<强>出现InvalidOperationException - 发生InvalidOperationException。它报告了一个&#34;收集被修改&#34;在foreach循环中

使用break语句,删除对象后。

前:

ArrayList list = new ArrayList(); 

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

答案 7 :(得分:2)

我想指出没有反映在任何答案中的其他情况。我在多线程应用程序中共享了一个 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() ,如注释中所示。这样enumerable在离开protected部分之前就已经被枚举了。

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

答案 8 :(得分: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);
            }

答案 9 :(得分: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);
}

答案 10 :(得分:1)

好的,所以帮助我的是向后迭代。我试图从列表中删除一个条目,但向上进行迭代,由于该条目不再存在,它使循环更加混乱:

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

                            myList.RemoveAt(x);

                        }

答案 11 :(得分:1)

在最坏的情况下,可接受的答案是不准确且不正确的。 如果在ToList() 期间进行了更改,则仍然会出现错误。除了lock,如果您有公共成员,还需要考虑性能和线程安全性,那么可以使用immutable types来解决问题。

通常,不可变类型意味着一旦创建就不能更改其状态。 因此您的代码应如下所示:

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进行操作,而不必担心集合会被修改。

答案 12 :(得分:0)

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

答案 13 :(得分:0)

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

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

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

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

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

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

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

答案 14 :(得分:0)

以下是需要采取特殊方法的特定情况:

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

在这种情况下,在每次枚举之前创建Dictionary(或Dictionary.Values)的副本可能会非常昂贵。解决此问题的想法是在多个枚举中重用同一缓存副本,并观察原始IEnumerator的{​​{1}}中是否有异常。枚举器将与复制的数据一起缓存,并在开始新的枚举之前被查询。如果发生异常,则将丢弃缓存的副本,并创建一个新副本。这是我对这个想法的实现:

Dictionary

用法示例:

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

不幸的是,.NET Core 3.0中的类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) { //... } 当前无法使用此想法,因为枚举时this class does not throw a Collection was modified exception以及方法RemoveClear被调用。我检查过的所有其他容器都表现一致。我系统地检查了这些课程: DictionaryList<T>Collection<T>ObservableCollection<T>HashSet<T>SortedSet<T>Dictionary<T,V>。 .NET Core中只有SortedDictionary<T,V>类的上述两个方法不会使枚举无效。


更新:我通过比较缓存和原始集合的长度来解决上述问题。此修补程序假定字典将作为参数直接传递给Dictionary的构造函数,并且其标识不会被SafeEnumerable这样的投影隐藏(例如)。

答案 15 :(得分:0)

当函数仍在执行时再次调用该函数时,这种方式应该可以解决并发的情况(并且项仅需要使用一次):

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

如果在仍在执行项目时调用该函数,则项目将在使用后立即被删除,因此不会从第一个开始再次重复。 对于小型列表,应该不会对性能产生太大影响。

答案 16 :(得分:0)

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

答案 17 :(得分:-1)

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