Threading and IEnumerable; "Collection Was Modified&quo
本文关键字:Was Modified quo Collection quot and Threading IEnumerable | 更新日期: 2023-09-27 18:02:49
假设在下面的类中,一个线程获得IEnumerable-object并开始遍历元素。在迭代的过程中,另一个线程出现并通过add方法向library_entries中添加一个新条目。在迭代中是否会抛出"Collection被修改"异常?或者锁会阻止元素的添加,直到迭代完成?或不?
谢谢!
public static class Library
{
private static List<string> library_entries = new List<string>(1000000);
public static void Add(string entry)
{
lock (library_entries)
library_entries.Add(entry);
}
public static IEnumerable<string> GetEntries()
{
return library_entries.Where(entry => !string.IsNullOrEmpty(entry));
}
}
不,你不会得到异常,你正在使用Linq查询。更糟糕的是,它将不可预测地失败。最典型的结果是同一项被枚举两次,尽管当List在Add()调用期间重新分配其内部存储(包括IndexOutOfRangeException)时,任何事情都有可能发生。一周一次,差不多。
调用GetEntries()并使用枚举数的代码也必须获得锁。锁定Where()表达式还不够好。除非你创建了一个列表的副本
锁定根本没有帮助,因为迭代不使用锁定。我建议重写GetEntries()
函数以返回一个副本。
public static IEnumerable<string> GetEntries()
{
lock(lockObj)
{
return library_entries.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
}
}
注意,这会返回一个一致的快照。也就是说,当你迭代时,它不会返回新添加的对象。
我更喜欢锁一个私有对象,它的唯一目的是锁,但由于列表是私有的,这不是真正的问题,只是一个风格问题。
你也可以这样写你自己的迭代器:
int i=0;
bool MoveNext()
{
lock(lockObj)
{
if(i<list.Count)
return list[i];
i++;
}
}
如果这是一个好主意取决于你的访问模式,锁争用,列表的大小,…您可能还需要使用读写锁来避免许多读访问的争用。
静态GetEntries
方法不会对静态library_entries
集合执行任何锁=>它不是线程安全的,任何来自多个线程的并发调用都可能中断。事实上,您锁定了Add方法,但枚举不是线程安全的操作,所以如果您打算并发地调用GetEntries方法,您也必须锁定它。另外,因为这个方法返回一个IEnumerable<T>
,所以它不会对实际列表做任何事情,直到您开始枚举可能在GetEntries
方法之外的列表。因此,您可以在LINQ链接的末尾添加.ToList()
调用,然后锁定整个操作。
是的,会抛出一个异常-您没有锁定一个普通对象。此外,GetEntries
方法中的锁将是无用的,因为该调用会立即返回。锁定必须在迭代时发生