【问题标题】:IEnumerable<T> thread safety?IEnumerable<T> 线程安全?
【发布时间】:2011-05-12 13:30:45
【问题描述】:

我有一个填充List&lt;T&gt; 的主线程。此外,我创建了一个将在不同线程上执行的对象链,需要访问列表。原始列表在生成后将永远不会被写入。我的想法是将列表作为IEnumerable&lt;T&gt; 传递给在其他线程上执行的对象,主要是因为不允许那些实现这些对象的对象错误地写入列表。换句话说,如果保证不写入原始列表,那么多个线程在IEnumerable上使用.Whereforeach是否安全?

如果原始集合从未更改,我不确定迭代器本身是否是线程安全的。

【问题讨论】:

    标签: c# .net asp.net multithreading


    【解决方案1】:

    IEnumerable&lt;T&gt; 无法修改。那么什么可以是非线程安全的呢? (如果不修改实际的List&lt;T&gt;)。

    对于非线程安全,您需要写入和读取操作。

    “迭代器本身”为每个foreach 实例化。

    编辑:我稍微简化了我的答案,但@Eric Lippert 添加了有价值的评论。 IEnumerable&lt;T&gt; 没有定义修改方法,但这并不意味着访问操作符是线程安全的(GetEnumeratorMoveNext 等)最简单的例子:GetEnumerator 实现如下:

    • 每次都返回IEnumerator的相同实例
    • 重置它的位置

    更复杂的例子是缓存。

    这很有趣,但幸运的是我不知道任何标准类没有IEnumerable 的线程安全实现。

    【讨论】:

    • 我不确定迭代器本身是否在内部维护,例如在某个类级别的变量中,它位于集合中的哪个项目上。如果两个线程正在迭代,则该内部状态受到损害。这种想法一定是错的吧?
    • 你需要为每个线程使用不同的迭代器,是的,但它们都可以由同一个列表支持。
    • 是的,在这种情况下是这样。我在考虑调用Iterator i = list.iterator() 然后使用它的显式情况——在这种情况下,你想为每个线程创建一个新的迭代器。
    • 仅仅因为一个 IE 不能被调用者修改,并不能保证它的内部工作对于多个读者来说是线程安全的! IE 的实现可能包含缓存或其他涉及写入共享数据结构的性能增强算法,并且对于多个读取器而言,这些可能不是线程安全的。仅仅因为集合在逻辑上没有发生变异并不要求nothing在其枚举期间发生变异。
    • @Andrey:我不知道,但这并不意味着不存在。例如,考虑使用哈希表实现的字典,正如您所期望的那样,但有一个转折:当您访问靠近存储桶列表末尾的键时,它将该存储桶移动到存储桶的前面列表。这样的字典在编译器中很常见,因为编译器经常会出现相同的标识符在代码中多次出现的情况。在读取表时动态重写表内部结构以尝试使未来的读取速度更快,这可能是一个成功的胜利。
    【解决方案2】:

    每个调用 Where 或 foreach 的线程都有自己的枚举器——它们不会为同一个列表共享一个枚举器对象。因此,由于 List 没有被修改,并且每个线程都在使用自己的枚举数副本,因此应该不存在线程安全问题。

    您可以在一个线程中看到这一点 - 只需创建一个包含 10 个对象的列表,然后从该列表中获取两个枚举器。使用一个枚举器枚举 5 个项目,使用另一个枚举器枚举 5 个项目。您将看到两个枚举器仅枚举了前 5 个项目,而第二个枚举器并未从第一个枚举器停止的地方开始。

    【讨论】:

    • 我认为这就是让我失望的原因,为每个请求提供了一个新的迭代器,因此它不被共享。
    • +1 表示“调用 Where 或 foreach 的每个线程都有自己的枚举器” - 我在任何地方都找不到此信息。从文档中不清楚我是否可以找到这种情况。谢谢。
    • 任何人都可以找到“每个调用 Where 或 foreach 的线程都有自己的枚举数”here 的证明。
    • 这完全取决于类如何实现GetEnumerator 方法。不保证返回一个单独的实例。
    • @RufusL 你完全正确,这取决于类如何实现GetEnumerator()。然而List 确实 以这种方式实现它,并且有一个以不同方式实现它的类真的 很奇怪。太奇怪了,事实上,我会认为这是一个错误。
    【解决方案3】:

    只要您确定List 永远不会被修改,那么从多个线程中读取是安全的。这包括使用它提供的IEnumerator 实例。

    对于大多数集合来说都是如此。事实上,BCL 中的所有集合在枚举期间都应该是稳定的。换句话说,枚举器不会修改数据结构。我能想到一些晦涩难懂的案例,比如展开树,正在枚举它可能修改结构。同样,BCL 集合都没有这样做。

    【讨论】:

      【解决方案4】:

      如果您确定列表在创建后不会被修改,您应该通过将其转换为ReadOnlyCollection<T> 来保证这一点。当然,如果您保留只读集合使用的原始列表,您可以对其进行修改,但如果您将原始列表扔掉,您实际上会使其永久只读。

      来自集合的线程安全部分:

      只要不修改集合,ReadOnlyCollection 可以同时支持多个读取器。

      因此,如果您不再触摸原始列表并停止引用它,您可以确保多个线程可以毫无顾虑地读取它(只要您不做任何古怪的尝试再次修改它)。

      【讨论】:

        【解决方案5】:

        换句话说,如果保证不写入原始列表,那么多个线程在 IEnumerable 上使用 .Where 或 foreach 是否安全?

        是的,如果列表发生突变,这只是一个问题。

        但请注意,IEnumerable&lt;T&gt; 可以转换回列表然后修改。

        但还有另一种选择:将您的列表包装成 ReadOnlyCollection&lt;T&gt; 并传递它。如果您现在丢弃原始列表,您基本上创建了一个新的不可变列表。

        【讨论】:

        • 你是对的。然而,使用 ReadOnlyCollection 的缺点是 myList.Add() 没有编译时警告,例如,这不是 IEnumerable 的问题。现在我想知道我是否会使用我原来的 IList 并执行 .AsReadOnly().AsEnumerable()?
        • @e36 AFAIK ReadOnlyCollection 明确实现 Add。所以myList.Add 行不通。仅当 ((IList&lt;T&gt;)myList).Add 有效时。这对我来说已经足够类型安全了。
        【解决方案6】:

        如果您使用的是 net framework 4.5 或更高版本,这可能是一个很好的解决方案 http://msdn.microsoft.com/en-us/library/dd997305(v=vs.110).aspx

        (微软已经实现了线程安全的枚举)

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-01-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-11-26
          • 2011-08-17
          • 1970-01-01
          相关资源
          最近更新 更多