【问题标题】:is enumerator thread safe after getting with lock?获得锁后枚举器线程安全吗?
【发布时间】:2014-09-09 07:29:45
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 对于 T 列表,不,不是。您可能需要考虑不可变集合 (msdn.microsoft.com/en-us/library/dn385366(v=vs.110).aspx),或者至少考虑明确线程安全的集合,例如 ConcurrentQueue
  • 是的 ConcurrentQueue 似乎符合要求,谢谢!
  • 如果有人对 MS 如何完成 ConcurrentQueue 感兴趣,我在这里找到了他们的代码实现:dotnetframework.org/default.aspx/4@0/4@0/untmp/DEVDIV_TFS/Dev10/…(非常慢的网页)
  • 是否有人可以指出 ConcurrentQueue 如何实现其线程安全的文档?我假设它的实现会比我能做的更好,但很高兴看到微软的工程师做了什么。

标签: c# multithreading c#-4.0


【解决方案1】:

不,一点也不。这个lock 只同步访问_list.GetEnumerator 方法;枚举列表远不止于此。它包括读取IEnumerator.Current 属性,调用IEnumerator.MoveNext 等。

您要么需要锁定foreach(我假设您是通过 foreach 枚举的),要么您需要制作列表的副本。

更好的选择是查看开箱即用的Threadsafe collections

【讨论】:

  • 从链接中查看 ConcurrentQueue 这似乎比滚动我自己的列表更符合我的目的。
【解决方案2】:

如果我有多个线程正在向此列表添加数据(也在 lock() 块中),并且一个线程枚举此列表的内容 列表。当枚举线程完成时,它会清除列表。会吗 那么可以安全地使用从这个方法得到的枚举器。

没有。参考这里:http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.aspx

只要集合仍然存在,枚举器就保持有效 不变。如果对集合进行了更改,例如添加, 修改或删除元素,枚举数不可恢复 无效并且下一次调用 MoveNext 或 Reset 会引发 无效操作异常。如果集合在之间被修改 MoveNext 和 Current, Current 返回它设置为的元素, 即使枚举器已经失效。 枚举器不 拥有对收藏的独家访问权;因此,枚举 通过集合本质上不是线程安全的过程。 即使一个集合被同步,其他线程仍然可以修改 集合,这会导致枚举器抛出异常。到 在枚举期间保证线程安全,您可以锁定 在整个枚举期间收集或捕获异常 由其他线程所做的更改导致。

..

枚举器是指向它被请求的实例的列表副本还是总是指向列表本身,这 可能会也可能不会被另一个线程操作 枚举?

取决于收藏。请参阅Concurrent CollectionsConcurrent StackConcurrentQueueConcurrentBag 在调用 GetEnumerator() 时都会拍摄集合的快照并从快照中返回元素。基础集合可能会在不更改快照的情况下更改。另一方面,ConcurrentDictionary 不会拍摄快照,因此在迭代时更改集合将立即按照上述规则生效。

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

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

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

你说:

当枚举线程完成时,它会清除列表。

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

【讨论】:

    【解决方案3】:

    根据documentation,为了保证线程安全,您必须在整个迭代期间锁定collecton。

    枚举器没有对集合的独占访问权; 因此,通过集合枚举本质上不是 线程安全的过程。为了保证枚举期间的线程安全, 您可以在整个枚举期间锁定集合。允许 由多个线程访问的集合以进行读取和 写,你必须实现自己的同步。

    另一种选择,可能是定义您自己的自定义迭代器,并为每个线程创建一个新的实例。所以每个线程都会有它自己的Current 副本只读指向同一个集合的指针。

    【讨论】:

    • 我看不到具有当前属性的自定义迭代器如何使其线程安全? MoveNext呢?
    • @SriramSakthivel:如果你定义了自定义迭代器,它有它自己的迭代指针和索引,你会在该迭代器的不同实例之间创建一个隔离,这会导致 read-only 访问,正如我所言。
    • 你说得对,我的问题是当我们调用MoveNext 时,迭代器如何指向正确的索引?没有列表的副本是不可能的,因为当您调用MoveNext 时,列表中的项目可能已经更改(由另一个线程)。不是吗?
    • @SriramSakthivel:是的,但我说的是所有线程的只读访问。如果在那一刻所有线程都使用相同的迭代器类型来翻转集合,并且该迭代器类型是只读的,则通过实现,访问变为只读。
    • 如果有错误请纠正我。如果所有线程只读取(不写入),您几乎可以使用_list.GetEnumerator() 本身,它是线程安全的枚举,除非集合被修改。
    猜你喜欢
    • 1970-01-01
    • 2020-02-20
    • 2010-10-05
    • 1970-01-01
    • 1970-01-01
    • 2014-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多