【问题标题】:Lock vs. ToArray for thread safe foreach access of List collectionLock vs. ToArray 用于 List 集合的线程安全 foreach 访问
【发布时间】:2010-06-27 21:02:57
【问题描述】:

我有一个 List 集合,我想在多线程应用程序中对其进行迭代。每次迭代时我都需要保护它,因为它可以更改,并且我不希望在执行 foreach 时出现“集合已修改”异常。

这样做的正确方法是什么?

  1. 每次访问或循环时都使用锁定。我比较害怕死锁。也许我只是偏执于使用锁,不应该。如果我走这条路以避免死锁,我需要知道什么?锁相当有效吗?

  2. 每次执行 foreach 时,都使用 List.ToArray() 复制到数组。这会导致性能下降,但很容易做到。我担心内存抖动以及复制它的时间。只是显得过分。使用 ToArray 是线程安全的吗?

  3. 不要使用 foreach 而是使用 for 循环。每次我这样做以确保列表没有缩小时,我不需要进行长度检查吗?这看起来很烦人。

【问题讨论】:

标签: c# multithreading foreach locking toarray


【解决方案1】:

没有什么理由害怕死锁,它们很容易被发现。你的程序停止运行,死的赠品。你真正应该害怕的是线程竞争,当你不应该锁定时你会得到的那种错误。 非常难以诊断。

  1. 使用锁很好,只要确保在任何涉及该列表的代码中使用完全相同的锁定对象。就像从该列表中添加或删除项目的代码一样。如果该代码在迭代列表的同一线程上运行,那么您不需要锁。通常,这里发生死锁的唯一机会是如果您的代码依赖于线程状态,例如 Thread.Join(),同时它也持有该锁定对象。这应该很少见。

  2. 是的,迭代列表的副本始终是线程安全的,只要您在 ToArray() 方法周围使用锁。请注意,您仍然需要锁,没有结构改进。优点是您可以在短时间内持有锁,从而提高程序的并发性。缺点是它的 O(n) 存储要求,只有一个安全列表但不保护列表中的元素,以及总是有一个过时的列表内容视图的棘手问题。尤其是最后一个问题很微妙,很难分析。如果您无法推断出副作用,那么您可能不应该考虑这一点。

  3. 请务必将 foreach 检测种族的能力视为礼物,而不是问题。是的,一个显式的 for(;;) 循环不会抛出异常,它只会发生故障。就像两次迭代同一个项目或完全跳过一个项目。您可以通过向后迭代来避免重新检查项目的数量。只要其他线程仅调用行为类似于 ToArray() 的 Add() 而不是 Remove(),您就会得到陈旧的视图。并不是说这在实践中会起作用,索引列表也不是线程安全的。 List 将在必要时重新分配其内部数组。这只是行不通,而且会以不可预知的方式出现故障。

这里有两种观点。您可能会感到害怕并遵循常识,您将获得一个有效但可能不是最佳的程序。这是明智的,让老板高兴。或者,您可以自己试验并找出扭曲规则如何给您带来麻烦。这会让你开心,你会成为一个更好的程序员。但是你的生产力会受到影响。我不知道你的日程安排是怎样的。

【讨论】:

    【解决方案2】:

    如果您的列表数据大部分是只读的,您可以使用ReaderWriterLockSlim 允许多个线程同时安全地访问它

    您可以在此处找到Thread-Safe dictionary 的实现以帮助您入门。

    我还想提一下,如果您使用的是 .Net 4.0,BlockingCollection 类会自动实现此功能。 我希望我在几个月前就知道

    【讨论】:

    • ReaderWriterLockSlim 是一个很好的选择。它将允许多个线程同时枚举集合,这与只允许一个线程的简单锁不同。它也不会产生与 ToArray() 相关的开销。
    【解决方案3】:

    您还可以考虑使用不可变数据结构 - 将您的列表视为值类型。

    如果可能,使用不可变对象可能是多线程编程的绝佳选择,因为它们消除了所有笨拙的锁定语义。本质上,任何会改变对象状态的操作都会创建一个全新的对象。

    例如我整理了以下内容来展示这个想法。抱歉,这绝不是参考代码,而且开始有点长了。

    public class ImmutableWidgetList : IEnumerable<Widget>
    {
        private List<Widget> _widgets;  // we never modify the list
    
        // creates an empty list
        public ImmutableWidgetList()
        {
            _widgets = new List<Widget>();
        }
    
        // creates a list from an enumerator
        public ImmutableWidgetList(IEnumerable<Widget> widgetList)
        {
            _widgets = new List<Widget>(widgetList);
        }
    
        // add a single item
        public ImmutableWidgetList Add(Widget widget)
        {
            List<Widget> newList = new List<Widget>(_widgets);
    
            ImmutableWidgetList result = new ImmutableWidgetList();
            result._widgets = newList;
            return result;
        }
    
        // add a range of items.
        public ImmutableWidgetList AddRange(IEnumerable<Widget> widgets)
        {
            List<Widget> newList = new List<Widget>(_widgets);
            newList.AddRange(widgets);
    
            ImmutableWidgetList result = new ImmutableWidgetList();
            result._widgets = newList;
            return result;
        }
    
        // implement IEnumerable<Widget>
        IEnumerator<Widget> IEnumerable<Widget>.GetEnumerator()
        {
            return _widgets.GetEnumerator();
        }
    
    
        // implement IEnumerable
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _widgets.GetEnumerator();
        }
    }
    
    • 我包含了IEnumerable&lt;T&gt; 以允许foreach 实现。
    • 您提到您担心创建新列表的空间/时间性能,所以这可能不适合您。
    • 您可能还想实现IList&lt;T&gt;

    【讨论】:

      【解决方案4】:

      一般来说,出于性能原因,集合不是线程安全的,哈希表除外。您必须使用 IsSynchronized 和 SyncRoot 使它们成为线程安全的。见herehere

      来自 msdn 的示例

      ICollection myCollection = someCollection;
      lock(myCollection.SyncRoot)
      {
          foreach (object item in myCollection)
          {
              // Insert your code here.
          }
      }
      

      编辑:如果你使用.net 4.0,你可以使用concurrent collections

      【讨论】:

        【解决方案5】:

        请使用lock(),除非您有其他复印理由。只有当您以不同的顺序请求多个锁时,才会发生死锁,例如:

        线程 1:

        lock(A) {
          // .. stuff
          // Next lock request can potentially deadlock with 2
          lock(B) {
            // ... more stuff
          }
        }
        

        线程 2:

        lock(B) {
          // Different stuff
          // next lock request can potentially deadlock with 1
          lock(A) {
            // More crap
          }
        }
        

        这里线程 1 和线程 2 有可能导致死锁,因为线程 1 可能持有 A 而线程 2 持有 B,并且在另一个释放其锁定之前两者都不能继续。

        如果您必须使用多个锁,请始终以相同的顺序执行。如果您只使用一个锁,那么您不会导致死锁......除非您在等待用户输入时持有锁,但这在技术上不是死锁并导致另一点:永远不要再持有锁比你绝对必须的。

        【讨论】:

          猜你喜欢
          • 2011-12-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-01-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-12-10
          相关资源
          最近更新 更多