Определить InvalidOperationException «Коллекция была изменена; операция перечисления может не выполняться.» - PullRequest
3 голосов
/ 12 августа 2011

У меня есть старое доброе 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.

EDIT

Просто добавьте, я мог бы 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 ]

5 голосов
/ 15 августа 2011

Вот что вы можете использовать, чтобы обернуть 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 голос
/ 12 августа 2011

Предполагая, что вы контролируете 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. Catch 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();
    }
}
0 голосов
/ 12 августа 2011

Поскольку вы знаете, что множественный поток может касаться route, предотвратите любое касание на маршруте с помощью lock() и всегда в lock () конвертируйте route.ToArray(), а затем используйте этот массив для цикла.Это потому, что вы, вероятно, чувствуете некоторые недостатки в производительности, если блокируете () весь цикл.Чтобы узнать, кто на самом деле касается коллекции, вы можете получить ее и отследить идентификатор потока в локальной переменной-члене в элементах добавления / удаления.

0 голосов
/ 12 августа 2011

Проблема, очевидно, не в написанном вами коде, поскольку вы не изменяете коллекцию при перечислении.

Каким-то образом происходит одна из двух вещей:

  1. Ваш route почему-то не является глубоким клоном, и некоторые потоки модифицируют ту же коллекцию, которую вы передаете вставленной логике (это должно быть легко отследить, если это где-то проблема кодирования, а не какое-то неприятное состояние гонки).
  2. Ваше исключение возникает именно в логике клонирования, а не в вставленном вами коде.

Попробуйте реализовать механизм блокировки при глубоком клонировании вашей коллекции и посмотрите, решит ли это проблему.

...