Почему перечисление через коллекцию вызывает исключение, а циклический просмотр ее элементов не - PullRequest
4 голосов
/ 11 апреля 2009

Я тестировал некоторые конструкции синхронизации и заметил кое-что, что меня смутило. Когда я одновременно перечислял коллекцию во время записи в нее, она выдавала исключение (это ожидалось), но когда я просматривал коллекцию, используя цикл for, это не так. Может кто-нибудь объяснить это? Я думал, что Список не позволяет читателю и писателю работать одновременно. Я ожидал бы, что циклический просмотр коллекции будет демонстрировать то же поведение, что и при использовании перечислителя.

ОБНОВЛЕНИЕ: Это чисто академическое упражнение. Я понимаю, что перечислять список плохо, если он записывается одновременно. Я также понимаю, что мне нужна конструкция синхронизации. Мой вопрос снова был о том, почему операция одна выдает исключение, как ожидалось, а другая - нет.

Код ниже:

   class Program
   {
    private static List<string> _collection = new List<string>();
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddItems), null);
        System.Threading.Thread.Sleep(5000);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DisplayItems), null);
        Console.ReadLine();
    }

    public static void AddItems(object state_)
    {
        for (int i = 1; i <= 50; i++)
        {
            _collection.Add(i.ToString());
            Console.WriteLine("Adding " + i);
            System.Threading.Thread.Sleep(150);
        }
    }

    public static void DisplayItems(object state_)
    {
        // This will not throw an exception
        //for (int i = 0; i < _collection.Count; i++)
        //{
        //    Console.WriteLine("Reading " + _collection[i]);
        //    System.Threading.Thread.Sleep(150);
        //}

        // This will throw an exception
        List<string>.Enumerator enumerator = _collection.GetEnumerator();
        while (enumerator.MoveNext())
        {
            string value = enumerator.Current;
            System.Threading.Thread.Sleep(150);
            Console.WriteLine("Reading " + value);
        }
    }
}

Ответы [ 7 ]

15 голосов
/ 11 апреля 2009

Вы не можете изменять коллекцию при перечислении. Это правило существует даже без учета проблем с потоками. От MSDN :

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

Цикл for, основанный на целых числах, на самом деле не является перечислителем. В большинстве сценариев это совершает одно и то же. Тем не менее, интерфейс для IEnumerator гарантирует, что вы можете перебирать всю коллекцию. Платформа реализует это внутренне, генерируя исключение, если вызов MoveNext происходит после изменения коллекции. Это исключение выдается объектом перечислителя.

Цикл for, основанный на целых числах, проходит только через список чисел. Когда вы индексируете коллекцию по целому числу, вы просто получаете элемент в этой позиции. Если что-то было вставлено или удалено из списка, вы можете пропустить элемент или запустить один и тот же элемент дважды. Это может быть полезно в определенных ситуациях, когда вам нужно изменить коллекцию при ее обходе. Цикл for не имеет объекта-перечислителя для гарантии контракта IEnumerator, поэтому исключение не выдается.

2 голосов
/ 11 апреля 2009

Чтобы ответить на ваш актуальный вопрос ...

При перечислении вы получите IEnumerator, который привязан к состоянию списка, как это было, когда вы его запрашивали. Дальнейшие операции работают с перечислителем (MoveNext, Current).

При использовании цикла for вы выполняете последовательность вызовов, чтобы получить определенный элемент по индексу. Нет внешнего контекста, такого как перечислитель, который знает, что вы находитесь в цикле. Как известно всей коллекции, вы запрашиваете только один предмет. Поскольку коллекция никогда не передавала перечислитель, у нее нет возможности узнать, что причина, по которой вы запрашиваете элемент 0, затем элемент 1, затем элемент 2 и т. Д., Заключается в том, что вы просматриваете список.

Если вы гадите со списком в то же время, как ходите по нему, вы получите ошибки в любом случае. Если вы добавляете элементы, цикл for может пропустить некоторые молча, а цикл foreach сработает. Если вы удаляете элементы, цикл for может выкинуть индекс из диапазона, если вам не повезло, но, вероятно, будет работать большую часть времени.

Но я думаю, что вы понимаете все это, ваш вопрос был просто, почему два способа итерации вели себя по-разному. Ответ на это - состояние коллекции известно (коллекции), когда вы вызываете GetEnumerator в одном случае, и когда вы вызываете get_Item в другом случае.

1 голос
/ 11 апреля 2009

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

Когда вы просто зацикливаете список, нет ничего, что отслеживает версию, поэтому нет ничего, что ловит, что список изменился.

Если вы измените список во время его зацикливания, вы можете получить нежелательные эффекты, от которых перечислитель защищает вас. Например, если вы удалите элемент из списка без изменения индекса цикла, чтобы он все еще указывал на тот же элемент, вы можете пропустить элементы в цикле. Аналогично, если вы вставляете элементы без корректировки индекса, вы можете повторять один и тот же элемент более одного раза.

1 голос
/ 11 апреля 2009

Разница в том, что когда вы говорите, что «циклически просматриваете коллекцию», вы фактически не циклически просматриваете коллекцию, вы перебираете целые числа от 1 до 50 и добавляете в коллекцию эти индексы. Это никак не влияет на тот факт, что числа от 1 до 50 все еще существуют.

Когда вы перечисляете список, вы перечисляете элементы, а не индексы. Поэтому, когда вы добавляете элементы при перечислении, вы лишаете законной силы перечисление. Он построен таким образом, чтобы предотвратить случаи, подобные тому, что вы делаете, когда потенциально вы могли бы перечислить элемент 6 в списке одновременно с вставкой элемента в индекс 6, где вы могли бы перечислить потенциально старый или новый элемент, или некоторые неопределенное состояние.

Ищите «потокобезопасный» список, если вы хотите это сделать, но будьте готовы к тому, чтобы справиться с неточностями чтения + записи одновременно:)

1 голос
/ 11 апреля 2009

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

Получите новый перечислитель, когда вы начнете свою функцию отображения, и заблокируйте список, пока это происходит. В качестве альтернативы, сделайте глубокую копию своего списка в новый список _displayCollection и перечислите его в этой отдельной коллекции, в которую не будут записываться, кроме как заполненные до начала процесса отображения. Надеюсь, это поможет.

0 голосов
/ 11 апреля 2009

Вы не можете изменить коллекцию, ПРОСЧИТЫВАЯ через нее.

проблема в том, что вы начинаете перечислять, когда ваша коллекция НЕ ПОЛНА, и пытаетесь ПРОДЕЛАТЬ ДОБАВЛЕНИЕ ПУНКТОВ ПРИ ПОМЕЩЕНИИ

0 голосов
/ 11 апреля 2009

Код неисправен, так как вы спите в течение 5 секунд, но не все элементы были добавлены в список. Это означает, что вы начинаете отображать элементы в одном потоке до того, как первый поток завершит добавление элементов в список, что приведет к изменению базовой коллекции и аннулированию перечислителя.

Удаление Thread.Sleep из кода добавления подчеркивает это:

public static void AddItems(object state_)
{     
   for (int i = 1; i <= 50; i++)      
   {        
       _collection.Add(i.ToString());      
       Console.WriteLine("Adding " + i);  
   }   
} 

Вместо того, чтобы спать, вы должны использовать механизм синхронизации, который ожидает, пока первый поток завершит свою работу по добавлению элементов.

...