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

cdo*_*ner 861 c# concurrency wcf dictionary thread-safety

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

这是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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Jar*_*Par 1542

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

foreach(Subscriber s in subscribers.Values)
Run Code Online (Sandbox Code Playgroud)

foreach(Subscriber s in subscribers.Values.ToList())
Run Code Online (Sandbox Code Playgroud)

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

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

  • @CoffeeAddict:问题是在`foreach`循环中修改了`subscribers.Values`.调用`subscribers.Values.ToList()`将`subscribers.Values`的值复制到`foreach`开头的单独列表中.没有其他任何东西可以访问这个列表*(它甚至没有变量名!)*,所以没有什么可以在循环内修改它. (199认同)
  • 我不明白为什么你做了ToList以及为什么修复了一切 (60认同)
  • 请注意,如果在执行"ToList"时修改了集合,则"ToList"也会抛出. (29认同)
  • BTW .ToList()存在于System.Core dll中,它与.NET 2.0应用程序不兼容.因此,您可能需要将目标应用程序更改为.Net 3.5 (14认同)
  • 我很确定思考并不能解决这个问题,但只是让它更难以重现.`ToList`不是原子操作.甚至更有趣的是,"ToList"在内部基本上将自己的`foreach`复制到一个新的列表实例中,这意味着你通过添加一个额外的(虽然更快)`foreach`迭代来修复一个`foreach`问题. (11认同)
  • 这很棒.我使用ArrayList做了它,它也完美地工作(显然与ToArray()) (5认同)
  • 我认为关于ToList不是原子的担忧是非常奇怪的.这不是多线程代码.认为循环可能在ToList完成之前开始,从根本上误解了编程语言的工作原理.它不会随机多线程部分代码,也不会乱序执行代码.此外,InstanceContextMode.Single确保它一次只处理一个请求. (3认同)

Mit*_*eat 111

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

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
Run Code Online (Sandbox Code Playgroud)


x40*_*000 64

在我看来,更有效的方法是使用另一个列表,声明您将"要删除"的内容放入其中.然后在完成主循环(没有.ToList())之后,在"要删除"列表上执行另一个循环,在发生时删除每个条目.所以在你的课堂上你添加:

private List<Guid> toBeRemoved = new List<Guid>();
Run Code Online (Sandbox Code Playgroud)

然后你将它改为:

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 );
}
Run Code Online (Sandbox Code Playgroud)

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

  • 如果你有一个更大的收藏品,我期待这个值得考虑.如果它很小我可能只是ToList并继续前进. (8认同)

Moh*_*and 41

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

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
Run Code Online (Sandbox Code Playgroud)

  • 问题是,对于大型应用程序,锁可能是一个主要的性能损失 - 最好在`System.Collections.Concurrent`命名空间中使用集合. (9认同)
  • @JCoombs你可能正在修改,可能会在锁本身内重新分配`MarkerFrequencies`字典,这意味着原始实例不再被锁定.也可以尝试使用`for`而不是`foreach`,参见[this](http://stackoverflow.com/questions/9925083/collection-was-modified-enumeration-operation-may-not-execute)和[this ](http://stackoverflow.com/questions/16759964/collection-was-modified-enumeration-operation-may-not-execute-lock-is-being-us).如果能解决问题,请告诉我. (4认同)
  • 这是一个完整的例子吗?我有一个类(下面的_dictionary obj),它包含一个名为MarkerFrequencies的通用Dictionary <string,int>,但这样做并没有立即解决崩溃:lock(_dictionary.MarkerFrequencies){foreach(KeyValuePair <string,int> pair in _dictionary.MarkerFrequencies){...}} (2认同)

ope*_*ree 26

为什么这个错误?

通常,.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;
}
Run Code Online (Sandbox Code Playgroud)

这是一篇关于此解决方案的博客文章.

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


joe*_*joe 11

在最坏的情况下,接受的答案是不精确和不正确的。如果在 期间进行更改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
        }
    }
    //.........
}
Run Code Online (Sandbox Code Playgroud)

如果您有公共成员,这会特别有用。其他类始终可以foreach使用不可变类型,而不必担心集合被修改。


Mar*_*ven 10

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

for (int x = myList.Count - 1; x > -1; x--)
{
    myList.RemoveAt(x);
}
Run Code Online (Sandbox Code Playgroud)


Jot*_*aBe 8

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

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;
}
Run Code Online (Sandbox Code Playgroud)

一般来说,它工作得很好,但有时我会遇到异常。问题是 LINQ 的一个微妙之处:此代码返回一个IEnumerable<Info>,在离开受锁保护的部分后仍然没有枚举该值。因此,它可以在枚举之前被其他线程更改,从而导致异常。解决方案是强制枚举,例如如.ToList()注释中所示。这样,可枚举项在离开受保护部分之前就已经被枚举了。

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


小智 5

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

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

  • @Zapnologica的区别是-您不会*枚举*列表-而不是进行for / each,而是进行for / next并按整数访问它-您绝对可以修改列表a在for / next循环中,但永远不在for / each循环中(因为for / each *枚举*)-您也可以在for / next中向前进行,前提是您有额外的逻辑来调整计数器等。 (4认同)

小智 5

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

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

前任:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
Run Code Online (Sandbox Code Playgroud)