从另一个线程修改列表而迭代(C#)

问题描述:

我循环通过用foreach元素的列表,像这样:从另一个线程修改列表而迭代(C#)

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

然而,在另一个线程我打电话类似

aList.Remove(Element); 

在运行时,这会导致InvalidOperationException:集合被修改;枚举操作可能不会执行。处理这个问题的最佳方式是什么?(即使以性能为代价,我会认为它相当简单)?

谢谢!

线程A:

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

线程B:

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

这ofcourse是表现非常糟糕。

+0

谢谢,就像一个魅力:)但愿我不会通过使用获得任何性能问题.. – 2012-04-04 19:33:54

处理这个问题的最佳方法是什么?(即使牺牲性能,我会认为它很简单)?

基本上:不要尝试从多个线程修改非线程安全的集合而不锁定。你迭代的事实在这里大部分是不相关的 - 它只是帮助你更快地找到它。对于两个线程同时呼叫Remove都是不安全的。

要么使用线程安全的集合,如ConcurrentBag确保只有一个线程执行任何与集合在同一时间。

+0

没想到这一点,但是现在你说,这是有道理的。谢谢,我稍后可能会使用它,但对于这种情况,锁定工作正常。 – 2012-04-04 19:38:30

,如果你只是想避免异常使用

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

知晓 DoSomething的()也被执行的情况下 在其他线程被删除的元素

+1

不幸的是'ToArray'和'Remove'可能会导致不同种类的异常。虽然这是一个很好的想法!请参阅我的[回复](http://stackoverflow.com/a/10018399/158779)以了解如何使其正常工作。 – 2012-04-04 20:09:43

+0

哦...并欢迎来到stackoverflow :) – 2012-04-04 20:41:27

我不能具体从您的问题中得知,但(看起来)您正在对每个项目执行操作,然后将其删除。您可能需要查看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); 
    } 
} 

如果你有一个以上的读者更多,然后尝试读写锁(.NET 3.5+),斯利姆: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

如果你只是有一个读卡器,只是锁列表本身或私人对象(但不要锁定类型本身),如Eugen Rieck的答案所示。

方法#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(); 
} 
+0

我喜欢这里的各种解决方案,但是应该在前两个例子中注意到'aList'应该是类的内部对象,否则会发生死锁。 – 2015-05-07 01:31:25

+0

@DerekW:是的,这是一个很好的做法,值得注意。事实上,它不可能真正导致死锁,因为这种情况下的典型场景(嵌套锁等)不在此处。因此,除非另一段代码完全滥用'aList',否则可能发生的最糟糕的情况是增加锁争用。我认为整个“锁定”(这个)辩论有点夸张,并且偏向偏执一方,而不是有些人会承认。同样,尽管避免这种习语仍然是一种好习惯。 – 2015-05-07 02:53:23

+0

我也应该指出,当我正在考虑这个问题时,#3需要按照提出的完全遵循*。任何偏差(如决定分配给“本地”可能会被忽略)可能会导致随机和壮观的失败。对于我所知道的,它甚至可能在时空中撕裂一个整体。 – 2015-05-07 02:55:56