C# 收集被修改;枚举操作不能执行

C# 收集被修改;枚举操作不能执行,c#,wcf,concurrency,dictionary,thread-safety,C#,Wcf,Concurrency,Dictionary,Thread Safety,我无法弄清这个错误的根源,因为当附加调试器时,它似乎不会发生 收集被修改;枚举操作不能执行 下面是代码 这是Windows服务中的WCF服务器。每当发生数据事件时,服务就会调用方法NotifySubscribers() 当Windows窗体客户端订阅时,订阅服务器ID将添加到订阅服务器字典中,当客户端取消订阅时,它将从字典中删除。该错误发生在客户端取消订阅时(或之后)。似乎下次调用NotifySubscribers()方法时,foreach()循环会失败,主题行出现错误。该方法将错误写入应用程序

我无法弄清这个错误的根源,因为当附加调试器时,它似乎不会发生

收集被修改;枚举操作不能执行

下面是代码

这是Windows服务中的WCF服务器。每当发生数据事件时,服务就会调用方法
NotifySubscribers()

当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);
        }
    }
}
[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
公共类订阅服务器:ISubscriptionServer
{
专用静态IDictionary订户;
公共订阅服务器()
{            
订阅者=新字典();
}
公共订阅服务器(DataRecord sr)
{
foreach(订阅服务器中的订阅服务器s.Values)
{
尝试
{
s、 信号数据(sr);
}
捕获(例外e)
{
DCS.WriteToApplicationLog(即消息、,
System.Diagnostics.EventLogEntryType.Error);
取消订阅事件(s.ClientId);
}
}
}
公共Guid SubscribeEvent(字符串clientDescription)
{
订户=新订户();
subscriber.Callback=OperationContext.Current。
GetCallbackChannel();
subscriber.Add(subscriber.ClientId,subscriber);
返回subscriber.ClientId;
}
公共无效取消订阅事件(Guid客户端ID)
{
尝试
{
订阅服务器。删除(clientId);
}
捕获(例外e)
{
System.Diagnostics.Debug.WriteLine(“取消订阅错误”+
e、 信息);
}
}
}

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

foreach(Subscriber s in subscribers.Values)

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


调用
subscribers.Values.ToList()
subscribers.Values
的值复制到
foreach
开头的单独列表中。其他任何东西都无法访问此列表(它甚至没有变量名!),因此在循环中没有任何东西可以修改它。

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

有几种方法可以解决此问题,其中一种是将for循环更改为使用显式的
.ToList()


在我看来,一个更有效的方法是另列一张清单,你声明你把任何“要删除”的东西都放进去。然后,在完成主循环(不带.ToList())之后,在“待删除”列表上执行另一个循环,在每次执行时删除每个条目。因此,在您的课堂上,您添加:

private List<Guid> toBeRemoved = new List<Guid>();

这不仅可以解决您的问题,还可以避免您必须从字典中不断创建列表,如果字典中有很多订户,那么创建列表的成本会很高。假设在任何给定的迭代中要删除的订阅者列表低于列表中的总数,这应该会更快。但当然,如果您的具体使用情况有任何疑问,请随时对其进行分析,以确保情况属实。

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


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

您还可以锁定订阅服务器词典,以防止在循环时对其进行修改:

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

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

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

我也有同样的问题,当我使用
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);
}
//foreach(itemsToBeLast中的var项)
for(int i=0;iitem.Detach);
if(匹配项!=null)
{
itemsToBeLast.移除(匹配项);
继续;
}
添加(itemsToBeLast[i]);/(附件);
}
为什么会出现此错误? 通常.Net集合不支持同时枚举和修改。如果在枚举期间尝试修改集合列表,则会引发异常。因此,这个错误背后的问题是,我们不能在循环相同的过程中修改列表/字典

解决方案之一 如果我们使用字典的键列表来迭代字典,我们可以并行地修改字典对象,就像我们在遍历键集合和 而不是字典(并迭代其键集合)

例子
//从di获取密钥集合
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 );
}
 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
// 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);
}
//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;
}
ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }
ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }
ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
for (int x = myList.Count - 1; x > -1; x--)
{
    myList.RemoveAt(x);
}
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
        }
    }
    //.........
}
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)
{
    //...
}
 while (list.Count > 0)
 {
    string Item = list[0];
    list.RemoveAt(0);
 
    // do here what you need to do with item
 
 } 
 
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;
}