Изменение списка из другого потока во время итерации (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);
}
}