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

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

下面是代码。

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

当前回答

我想指出另一个没有在答案中反映出来的例子。我有一个字典<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,请注意始终在离开受保护区域之前实现查询。

其他回答

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

为什么会出现这个错误?

一般来说。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:为什么会发生此错误?

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

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

公认的答案在最坏的情况下是不精确和不正确的。如果在ToList()期间进行了更改,您仍然会得到一个错误。除了锁(如果您有一个公共成员,则需要考虑锁的性能和线程安全性)之外,一个合适的解决方案是使用不可变类型。

一般来说,不可变类型意味着一旦创建了它,就不能更改它的状态。 所以你的代码应该是这样的:

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,而不用担心集合被修改。

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

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