System.InvalidOperationException: коллекция была изменена

Я получаю следующее исключение при перечислении через очередь:

System.InvalidOperationException: коллекция была изменена; операция перечисления может не выполняться

вот фрагмент кода:

1:    private bool extractWriteActions(out List<WriteChannel> channelWrites)
2:    {
3:        channelWrites = new List<WriteChannel>();
4:        foreach (TpotAction action in tpotActionQueue)
5:        {
6:            if (action is WriteChannel)
7:            {
8:                channelWrites.Add((WriteChannel)action);
9:                lock(tpotActionQueue)
10:               {
11:                  action.Status = RecordStatus.Batched;
12:               }
13:           }
14:       }
15:       return (channelWrites.Count > 0);
16:   }

Я думаю, что понимаю проблему - изменение хеш-таблицы в action.Status = RecordStatus.Batched, что портит MoveNext() в перечислителе. Вопрос в том, как мне правильно реализовать этот «шаблон»?


person kermit_xc    schedule 20.07.2009    source источник
comment
Почему вы блокируете очередь? Этот код не имеет смысла для меня.   -  person Eric Lippert    schedule 20.07.2009
comment
@Kermit_xc: более важным моментом в документации счетчика является то, что перечислитель не имеет монопольного доступа к коллекции; поэтому перечисление в коллекции по своей сути не является потокобезопасной процедурой. Чтобы гарантировать безопасность потоков во время перечисления, вы можете заблокировать коллекцию на время всего перечисления. Чтобы разрешить доступ к коллекции нескольким потокам для чтения и записи, необходимо реализовать собственную синхронизацию.   -  person Stan R.    schedule 20.07.2009
comment
Правильно. Этот код не делает ничего даже отдаленно похожего на это.   -  person Eric Lippert    schedule 21.07.2009
comment
Рассмотрите возможность использования блокировки чтения-записи, если вам нужно синхронизировать чтения и записи в нескольких потоках.   -  person Eric Lippert    schedule 21.07.2009


Ответы (6)


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

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

private bool extractWriteActions(out List<WriteChannel> channelWrites)
    {
      lock(tpotActionQueue)
      {
        channelWrites = new List<WriteChannel>();
        foreach (TpotAction action in tpotActionQueue)
        {
            if (action is WriteChannel)
            {
                channelWrites.Add((WriteChannel)action);

                  action.Status = RecordStatus.Batched;

           }
        }
      }
       return (channelWrites.Count > 0);
   }
person Stan R.    schedule 20.07.2009
comment
черт! - только что проверено, получил другой поток, нажимающий элементы в коллекции, хотя всплывающие окна являются потокобезопасными. Однако я немного беспокоюсь о производительности «блокировки» на всей итерации. Это довольно чувствительно ко времени - может потребоваться полная переработка :{. Спасибо хоть! - person kermit_xc; 20.07.2009
comment
@kermit_xs: только что проверил, есть другой поток, подталкивающий элементы в коллекцию - это ваша проблема. переход от цикла foreach к циклу for не является вашей проблемой и не является ответом. при использовании foreach вам нужно заблокировать свою коллекцию перед перечислением. Перечислители не являются потокобезопасными. - person Stan R.; 20.07.2009

Я думаю, что у меня было подобное исключение при использовании цикла foreach в коллекции, когда я пытался удалить элементы из коллекции (или это мог быть список, я не могу вспомнить). В итоге я обошел это, используя цикл for. Возможно, попробуйте что-то вроде следующего:

for (int i=0; i<tpotActionQueue.Count(); i++)
{
    TpotAction action = tpotActionQueue.Dequeue();
    if (action is WriteChannel)
    {
        channelWrites.Add((WriteChannel)action);
        lock(tpotActionQueue)
        {
            action.Status = RecordStatus.Batched;
        }
    }
}
person Sarah Vessels    schedule 20.07.2009
comment
да, но этот метод не удаляет и не добавляет элементы в коллекцию. Проблема вне этого метода. - person Stan R.; 20.07.2009
comment
Большое спасибо, это имеет смысл ... попробую. - person kermit_xc; 20.07.2009
comment
Нет, Стэн, проблема в назначении action.Status, читайте документацию по перечислению для любой коллекции. Перечислитель остается действительным до тех пор, пока коллекция остается неизменной. Если в коллекцию вносятся изменения, такие как добавление, изменение или удаление элементов, перечислитель безвозвратно становится недействительным, и его поведение не определено. Здесь он модифицирует элемент - person HasaniH; 20.07.2009
comment
вы никогда не использовали foreach для инициализации списка? Список‹Тест› t = новый Список‹Тест›(); t.Добавить (новый тест()); t.Добавить (новый тест()); foreach (Элемент теста в t) { item.SomeName = SomeTest; } - person Stan R.; 20.07.2009
comment
это сработало отлично ... большое спасибо, это избавило меня от головной боли! - person jes9582; 26.05.2011

У вас нет определения для tpotActionQueue, но если это обычный List<TpotAction>, то эта строка не ваша проблема. Изменение коллекции — это добавление или удаление элементов, а не установка свойства содержащегося объекта.

У вас есть lock(tpotActionQueue) и тег потокобезопасности, поэтому я предполагаю, что есть еще один поток, добавляющий или удаляющий элементы из tpotActionQueue, пока вы перечисляете. Вероятно, вам нужно синхронизировать эти доступы.

person Mark Brackett    schedule 20.07.2009
comment
это была моя идея - изменение значения не должно испортить коллекцию. В конечном итоге я хотел удалить его из очереди, но вместо этого решил пометить его как «пакетный», чтобы его можно было позаботиться снаружи, в основном цикле очереди — хотя это тот же поток. - person kermit_xc; 20.07.2009
comment
установка свойства учитывается, msdn.microsoft.com/en-us/library/ 4a9449ty.aspx - person HasaniH; 20.07.2009

Как насчет доброты LINQy?

private bool extractWriteActions(out List<WriteChannel> channelWrites)
{

   channelWrites= tpotActionQueue.Where<WriteChannel>(x => x is WriteChannel).ToList()

   foreach(WriteChannel channel in channelWrites) {
      channel.Status = RecordStatus.Batched;
   }

  return ( channelWrites.Count > 0);
}
person Kirschstein    schedule 20.07.2009

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

for(int i = 0; i < tpotActionQueue.Length; i++)
{
     TpotAction action = tpotActionQueue[i];

     if (action is WriteChannel)
     {
        channelWrites.Add((WriteChannel)action);
        lock(tpotActionQueue)
        {
           action.Status = RecordStatus.Batched;
        }
     }
}

С уважением, Майк.

person Michael Ciba    schedule 20.07.2009
comment
это по-прежнему вызовет проблему в многопоточной среде, кажется, что tpotActionQueue является глобальной переменной, и другой поток может изменить ее во время вызова этого метода. - person Stan R.; 20.07.2009
comment
кроме того, вам разрешено изменять элемент в коллекции, поэтому я не вижу в этом смысла. - person Stan R.; 20.07.2009

Я думаю, у вас должен быть какой-то другой поток, модифицирующий tpotActionQueue, пока вы его перебираете. Поскольку вы блокируете эту очередь только внутри цикла for, это возможно.

person CodeGoat    schedule 20.07.2009