Изменение списка из другого потока во время итерации (C#)

Я просматриваю список элементов с foreach, например:

foreach (Type name in aList) {
   name.doSomething();
}

однако в другом потоке я вызываю что-то вроде

aList.Remove(Element);

во время выполнения это вызывает исключение InvalidOperationException: коллекция была изменена; операция перечисления может не выполняться. Каков наилучший способ справиться с этим (я бы улучшил его, чтобы быть довольно простым даже ценой производительности)?

спасибо!

6 ответов


Поток A:

lock (aList) {
  foreach (Type name in aList) {
     name.doSomething();
  }
}

Поток B:

lock (aList) {
  aList.Remove(Element);
}

это, конечно, очень плохо для производительности.


каков наилучший способ справиться с этим (я бы улучшил его, чтобы быть довольно простым даже за счет производительности)?

фундаментально: не пытайтесь изменить не потокобезопасную коллекцию из нескольких потоков без блокировки. Тот факт, что вы повторяете, здесь в основном не имеет значения - это просто помогло вам найти его быстрее. Было бы небезопасно, чтобы два потока оба вызывали Remove в то же время.

или использовать a потокобезопасная коллекция, такая как ConcurrentBag или убедитесь, что только один поток делает что-нибудь с коллекцией в то время.


Способ #1:

самый простой и наименее эффективный метод-создать критический Раздел для читателей и писателей.

// Writer
lock (aList)
{
  aList.Remove(item);
}

// Reader
lock (aList)
{
  foreach (T name in aList)
  {
    name.doSomething();
  }
}

Способ № 2:

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

// Writer
lock (aList)
{
  aList.Remove(item);
}

// Reader
List<T> copy;
lock (aList)
{
  copy = new List<T>(aList);
}
foreach (T name in copy)
{
  name.doSomething();
}

Способ #3:

все зависит от ваша конкретная ситуация, но обычно я имею дело с этим, чтобы сохранить главную ссылку на коллекцию неизменной. Таким образом, вам никогда не придется синхронизировать Доступ со стороны читателя. Писательская сторона вещей нуждается в lock. Стороне читателя ничего не нужно, что означает, что читатели остаются очень параллельными. Единственное, что вам нужно сделать, это пометить aList ссылка как volatile.

// Variable declaration
object lockref = new object();
volatile List<T> aList = new List<T>();

// Writer
lock (lockref)
{
  var copy = new List<T>(aList);
  copy.Remove(item);
  aList = copy;
}

// Reader
List<T> local = aList;
foreach (T name in local)
{
  name.doSomething();
}

Если у вас более одного reader, затем попробуйте блокировку Reader-Writer (.Net 3.5+), Slim: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

Если у вас есть только один читатель, просто блокировка самого списка или частный объект (но не блокируйте сам тип), как показано в ответе Евгения Рика.


Если вы просто хотите избежать использования исключения

foreach (Type name in aList.ToArray()) 
{ name.doSomething(); }

имейте в виду сделать() выполняется также в случае, если элемент был удален в другой ветке


Я не могу точно сказать из вашего вопроса, но (похоже) вы выполняете действие над каждым элементом, а затем удаляете его. Возможно, вы захотите заглянуть в BlockingCollection<T>, который имеет вызов метода GetConsumingEnumerable() чтобы увидеть, если это хорошо подходит для вас. Вот небольшой образец.

void SomeMethod()
{
    BlockingCollection<int> col = new BlockingCollection<int>();

    Task.StartNew( () => { 

        for (int j = 0; j < 50; j++)
        {
            col.Add(j);
        }

        col.CompleteAdding(); 

     });

    foreach (var item in col.GetConsumingEnumerable())
    {
       //item is removed from the collection here, do something
       Console.WriteLine(item);
    }
}