协慌网

登录 贡献 社区

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

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

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

答案

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

foreach(Subscriber s in subscribers.Values)

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

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

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

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

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

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

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

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