从另一个线程修改列表而迭代(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是表现非常糟糕。
处理这个问题的最佳方法是什么?(即使牺牲性能,我会认为它很简单)?
基本上:不要尝试从多个线程修改非线程安全的集合而不锁定。你迭代的事实在这里大部分是不相关的 - 它只是帮助你更快地找到它。对于两个线程同时呼叫Remove
都是不安全的。
要么使用线程安全的集合,如ConcurrentBag
或确保只有一个线程执行任何与集合在同一时间。
没想到这一点,但是现在你说,这是有道理的。谢谢,我稍后可能会使用它,但对于这种情况,锁定工作正常。 – 2012-04-04 19:38:30
,如果你只是想避免异常使用
foreach (Type name in aList.ToArray())
{ name.doSomething(); }
知晓 DoSomething的()也被执行的情况下 在其他线程被删除的元素
不幸的是'ToArray'和'Remove'可能会导致不同种类的异常。虽然这是一个很好的想法!请参阅我的[回复](http://stackoverflow.com/a/10018399/158779)以了解如何使其正常工作。 – 2012-04-04 20:09:43
哦...并欢迎来到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();
}
我喜欢这里的各种解决方案,但是应该在前两个例子中注意到'aList'应该是类的内部对象,否则会发生死锁。 – 2015-05-07 01:31:25
@DerekW:是的,这是一个很好的做法,值得注意。事实上,它不可能真正导致死锁,因为这种情况下的典型场景(嵌套锁等)不在此处。因此,除非另一段代码完全滥用'aList',否则可能发生的最糟糕的情况是增加锁争用。我认为整个“锁定”(这个)辩论有点夸张,并且偏向偏执一方,而不是有些人会承认。同样,尽管避免这种习语仍然是一种好习惯。 – 2015-05-07 02:53:23
我也应该指出,当我正在考虑这个问题时,#3需要按照提出的完全遵循*。任何偏差(如决定分配给“本地”可能会被忽略)可能会导致随机和壮观的失败。对于我所知道的,它甚至可能在时空中撕裂一个整体。 – 2015-05-07 02:55:56
谢谢,就像一个魅力:)但愿我不会通过使用获得任何性能问题.. – 2012-04-04 19:33:54