3

我有一个很好的老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;
}

然后我在这个断言上失败了,因为在 ToList() 之前发生了一个机会......我需要尝试找出发生变化的地方

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

4 回答 4

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);
    }
}
于 2011-08-15T16:37:45.180 回答
1

假设您控制Add(TVisit)/Remove(TVisit)控制TRoute基础集合:

  1. 扩展您TRoute.IEnumerator<TVisit> GetEnumerator()以设置 AutoResetEvent 或 Mutex
  2. 扩展您的Add(TVisit)/Remove(TVisit)方法以等待具有零超时的事件/互斥锁

    if(!autoReseEvent.WaitOne(0)) throw new MyException();
    
  3. 抓住MyExpcetion,您将获得堆栈跟踪并更改来源。

更新: 这种方法的问题是何时发布事件/互斥锁。您可能必须使用如下所示的新类来装饰您的枚举器:

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;
}

还有信封本身:

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();
    }
}
于 2011-08-12T12:31:57.940 回答
0

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

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

  1. route在某种程度上不是一个深度克隆,并且某些线程正在修改您传递给粘贴逻辑的同一个集合(应该很容易跟踪它是某个地方的编码问题,而不是一些讨厌的竞争条件)。
  2. 您的异常正是在克隆逻辑中引发的,而不是在您粘贴的代码中。

尝试在深度克隆您的收藏时实施一些锁定机制,看看是否能解决问题。

于 2011-08-12T12:13:41.137 回答
0

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

于 2011-08-12T13:03:07.853 回答