我想知道返回的枚举器是否是线程安全的:
public IEnumerator<T> GetEnumerator()
{
lock (_sync) {
return _list.GetEnumerator();
}
}
如果我有多个线程将数据(也在lock()
块中)添加到此列表中,并且一个线程枚举此列表的内容。枚举线程完成后,清除列表。那么使用从这种方法得到的枚举器是否安全。
即。枚举器是否指向要求它的实例的列表副本,或者它总是指向列表本身,在枚举期间可能会或可能不会被另一个线程操纵?
如果枚举器不是线程安全的,那么我能看到的唯一其他操作方法是创建列表的副本并返回该列表。然而,这并不理想,因为它会产生大量垃圾(这种方法每秒调用约60次)。
答案 0 :(得分:5)
不,一点也不。此lock
仅同步_list.GetEnumerator
方法的访问权限;枚举列表的地方远不止于此。它包括阅读IEnumerator.Current
属性,调用IEnumerator.MoveNext
等。
您需要锁定foreach
(我假设您通过foreach枚举),或者您需要复制列表。
更好的选择是看一下开箱即用的Threadsafe collections。
答案 1 :(得分:2)
根据documentation,为了保证线程安全,你必须在整个迭代过程中锁定collecton。
枚举器没有对集合的独占访问权限; 因此,通过集合枚举本质上不是一个 线程安全的程序。为了在枚举期间保证线程安全, 您可以在整个枚举期间锁定集合。允许 多个线程要访问的集合,用于阅读和 写作,你必须实现自己的同步。
另一个选项,可能是定义你自己的自定义迭代器,并为每个线程创建一个新的实例。因此,每个线程都有自己的Current
只读指向同一集合的指针。
答案 2 :(得分:1)
如果我有多个线程将数据(也在lock()块中)添加到此列表中,并且一个线程枚举此内容 名单。枚举线程完成后,清除列表。会吗 然后可以安全地使用从这种方法中获得的枚举器。
没有。请参阅此处的参考:http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.aspx
只要收集仍然存在,枚举器仍然有效 不变。如果对集合进行了更改,例如添加, 修改或删除元素,枚举器是不可恢复的 无效,下次调用MoveNext或Reset会抛出一个 InvalidOperationException异常。如果在之间修改了集合 MoveNext和Current,Current返回它设置的元素, 即使枚举器已经失效。 普查员没有 拥有该系列的独家使用权;因此,列举 通过集合本质上不是一个线程安全的过程。 即使集合已同步,其他线程仍可以修改 集合,导致枚举器抛出异常。至 在枚举期间保证线程安全,你可以锁定 整个枚举期间的集合或捕获异常 由其他线程所做的更改产生的结果。
..
枚举器指向要求它的实例的列表副本,或者它总是指向列表本身, 在其期间可能会或可能不会被另一个线程操纵 枚举?
取决于收藏品。请参阅Concurrent Collections。 Concurrent Stack,ConcurrentQueue,ConcurrentBag 在调用GetEnumerator()时都会捕获集合的快照,并从快照返回元素。底层集合可能会更改而不更改快照。另一方面, ConcurrentDictionary 不会拍摄快照,因此在迭代时更改集合会立即影响上述规则。
我在这种情况下有时使用的一个技巧是创建一个临时集合进行迭代,以便在使用快照时原始文件是免费的:
foreach(var item in items.ToList()) {
//
}
如果您的列表太大而导致GC流失,那么锁定可能是您最好的选择。如果锁定太重,如果可行,可以考虑每次切片的部分迭代。
你说:
枚举线程完成后,清除列表。
没有什么说你必须一次处理整个列表。您可以改为移除一系列项目,将它们移动到单独的枚举线程,让该过程重复。也许迭代和列表不是这里最好的模型。考虑使用ConcurrentQueue构建生产者和消费者模型,消费者只需稳定地删除要处理的项目而不进行迭代