识别InvalidOperationException“集合已被修改;枚举操作可能无法执行。”

时间:2011-08-12 11:56:50

标签: c# .net-2.0 invalidoperationexception

我有一个好的旧InvalidOperationException被抛出标准消息

  

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

问题是,枚举器不会自行修改,例如:

private TRoute _copyRoute(TRoute route)
{
    TRoute tempRoute = new TRoute();
    tempRoute.Initialize(route.Resource);

    foreach (TVisit visit in route)
    {
       tempRoute.Add(visit);
    }
    tempRoute.EndLocation = route.EndLocation;
    return tempRoute;
}

我的代码是多线程的(本例中大约12-15个线程),每个线程假设正在处理自己的路由深度克隆。显然某些地方出了问题,但是,我的问题是如何通过如此多的线程来追踪这一点?减少数量可以显着阻止问题出现。

在这种情况下,我的路由实例是一个IList,所以我可以在界面中添加东西。它下面有它自己的List实现。

修改

只是添加,我可以ToArray()或ToList()这个可能忽略这里的问题,但我真的不想这样做,我想找到原因。例如:

如果我将其更改为以下内容:

private TRoute _copyRoute(TRoute route)
{
    TRoute tempRoute = new TRoute();
    tempRoute.Initialize(route.Resource);

    foreach (TVisit visit in route.ToList())
    {
       tempRoute.Add(visit);
    }
    tempRoute.EndLocation = route.EndLocation;
    return tempRoute;
}

然后我在这个Assert上失败了,因为在ToList()之前发生了一次机会...我需要尝试找出发生了哪些变化

TRoute tempRoute1 = CopyRoute(route1);
TRoute tempRoute2 = CopyRoute(route2);
Debug.Assert(tempRoute1.Count == route1.Count);

4 个答案:

答案 0 :(得分:5)

这是你可以用来包装IList<T>的东西 - 它会在每次写操作时检查它是否在正确的线程上。当然,在另一个线程上进行迭代而在另一个线程上进行迭代仍然是不安全的,但我认为这不是问题。 (你总是可以在所有操作上调用CheckThread,而不仅仅是写作操作。)

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;

class ThreadAffineList<T> : IList<T>
{
    private readonly Thread expectedThread;
    private readonly IList<T> list;

    public ThreadAffineList(IList<T> list)
    {
        this.list = list;
        this.expectedThread = Thread.CurrentThread;
    }

    private void CheckThread()
    {
        if (Thread.CurrentThread != expectedThread)
        {
            throw new InvalidOperationException("Incorrect thread");
        }
    }

    // Modification methods: delegate after checking thread
    public T this[int index]
    {
        get { return list[index]; }
        set
        {
            CheckThread();
            list[index] = value;
        }
    }

    public void Add(T item)
    {
        CheckThread();
        list.Add(item);
    }

    public void Clear()
    {
        CheckThread();
        list.Clear();
    }

    public void Insert(int index, T item)
    {
        CheckThread();
        list.Insert(index, item);
    }

    public bool Remove(T item)
    {
        CheckThread();
        return list.Remove(item);
    }

    public void RemoveAt(int index)
    {
        CheckThread();
        list.RemoveAt(index);
    }

    // Read-only members
    public int Count { get { return list.Count; } }
    public bool IsReadOnly { get { return list.IsReadOnly; } }

    public IEnumerator<T> GetEnumerator()
    {
        return list.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public bool Contains(T item)
    {
        return list.Contains(item);
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        list.CopyTo(array, arrayIndex);
    }

    public int IndexOf(T item)
    {
        return list.IndexOf(item);
    }
}

答案 1 :(得分:1)

假设您控制了Add(TVisit) Remove(TVisit)基础集合的TRoute / TRoute.IEnumerator<TVisit> GetEnumerator()

  1. 扩展您的Add(TVisit)以设置AutoResetEvent或Mutex
  2. Remove(TVisit) / if(!autoReseEvent.WaitOne(0)) throw new MyException(); 方法扩展为等待事件/互斥锁,零超时

    MyExpcetion
  3. 捕获public IEnumerator<TVisit> GetEnumerator() { IEnumerator<TVisit> originEnum = // get it somehow from underlying collection IEnumerator<TVisit> evenlope = new DisposableEvenlope<TVisit>(originEnum); evenlope.Disposed += new EventHandler(/* do your magic and reset event/mutex here */); return evenlope; } ,您将获得堆栈跟踪并更改原点。

  4. <强>更新 此方法的问题是何时释放事件/互斥锁。您可能需要使用如下所示的新类来装饰您的枚举器:

    public class DisposableEvenlope<T> : IEnumerator<T>
    {
        private IEnumerator<T> _privateEnum;
    
        public event System.EventHandler Disposed;
    
        public DisposableEvenlope(IEnumerator<T> privateEnum)
        {
            _privateEnum = privateEnum;
        }
    
        public T Current
        {
            get { return _privateEnum.Current; }
        }
    
        public void Dispose()
        {
            Disposed(this, new System.EventArgs());
        }
    
        object IEnumerator.Current
        {
            get { return _privateEnum.Current; }
        }
    
        public bool MoveNext()
        {
            return _privateEnum.MoveNext();
        }
    
        public void Reset()
        {
            _privateEnum.Reset();
        }
    }
    

    信封本身:

    {{1}}

答案 2 :(得分:0)

问题显然不在你编写的代码中,因为你在枚举时没有修改集合。

不知何故发生了两件事之一:

  1. 你的route在某种程度上不是一个深层次的克隆,有些线程正在调整你传递给粘贴逻辑的同一个集合(应该很容易跟踪它是某个地方的编码问题,而不是一些令人讨厌的竞争条件)。
  2. 您的异常正是在克隆逻辑中提出的,而不是在您粘贴的代码中。
  3. 尝试在深入克隆您的集合时实施一些锁定机制,看看是否能解决问题。

答案 3 :(得分:0)

由于您知道多线程可以触及route,因此请阻止路由触及lock(),并始终使用锁定()转换route.ToArray(),然后使用该数组环。这是因为如果你锁定()整个循环,你可能会感觉到一些性能上的缺陷。要捕获谁真正触及集合,您可以派生它并跟踪添加/删除元素中的本地成员变量中的线程ID。