获得锁定后,枚举器线程安全吗?

时间:2014-07-18 08:16:11

标签: c# multithreading c#-4.0

我想知道返回的枚举器是否是线程安全的:

public IEnumerator<T> GetEnumerator()
{
    lock (_sync) {
        return _list.GetEnumerator();
    }
}

如果我有多个线程将数据(也在lock()块中)添加到此列表中,并且一个线程枚举此列表的内容。枚举线程完成后,清除列表。那么使用从这种方法得到的枚举器是否安全。

即。枚举器是否指向要求它的实例的列表副本,或者它总是指向列表本身,在枚举期间可能会或可能不会被另一个线程操纵?

如果枚举器不是线程安全的,那么我能看到的唯一其他操作方法是创建列表的副本并返回该列表。然而,这并不理想,因为它会产生大量垃圾(这种方法每秒调用约60次)。

3 个答案:

答案 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 StackConcurrentQueueConcurrentBag 在调用GetEnumerator()时都会捕获集合的快照,并从快照返回元素。底层集合可能会更改而不更改快照。另一方面, ConcurrentDictionary 不会拍摄快照,因此在迭代时更改集合会立即影响上述规则。

我在这种情况下有时使用的一个技巧是创建一个临时集合进行迭代,以便在使用快照时原始文件是免费的:

foreach(var item in items.ToList()) {
    //
}

如果您的列表太大而导致GC流失,那么锁定可能是您最好的选择。如果锁定太重,如果可行,可以考虑每次切片的部分迭代。

你说:

  

枚举线程完成后,清除列表。

没有什么说你必须一次处理整个列表。您可以改为移除一系列项目,将它们移动到单独的枚举线程,让该过程重复。也许迭代和列表不是这里最好的模型。考虑使用ConcurrentQueue构建生产者和消费者模型,消费者只需稳定地删除要处理的项目而不进行迭代