代码之家  ›  专栏  ›  技术社区  ›  Bryan Legend

锁与ToArray对列表集合的每次访问都是线程安全的

  •  22
  • Bryan Legend  · 技术社区  · 16 年前

    我有一个列表集合,我想在多线程应用程序中迭代它。每次迭代时我都需要保护它,因为它可能会被更改,而且我不希望在执行foreach时出现“collection was modified”异常。

    正确的方法是什么?

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

    2. 不要使用foreach,而是使用for循环。我不是每次都要做一次长度检查以确保列表不会缩小吗?那看起来很烦人。

    5 回复  |  直到 16 年前
        1
  •  46
  •   Hans Passant    14 年前

    没有什么理由害怕死锁,它们很容易被发现。你的程序停止运行,毫无意义。你真正应该害怕的是线程竞速,当你不锁定的时候你会遇到这种错误。 非常 很难诊断。

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

    2. 一定要把foreach检测种族的能力当作礼物,而不是问题。是,一个显式for(;;)循环不会抛出异常,它只会出现故障。比如重复同一个项目两次或者完全跳过一个项目。通过向后迭代,可以避免重新检查项目数。只要其他线程只调用Add()而不调用Remove(),其行为类似于ToArray(),您就会得到过时的视图。这在实践中是行不通的,索引列表也不是线程安全的。列表<&燃气轮机;将在必要时重新分配其内部数组。这只是不工作和故障的不可预知的方式。

        2
  •  11
  •   Laramie    16 年前

    如果您的列表数据大部分是只读的,那么您可以允许多个线程使用 ReaderWriterLockSlim

    Thread-Safe dictionary

    我还想提到,如果您使用的是.NET4.0 BlockingCollection 类自动实现此功能。 我要是早知道就好了 几个月前的事了!

        3
  •  5
  •   Robert Paulson    16 年前

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

    e、 为了证明这个想法,我突然提出了以下几点。我很抱歉,这绝对不是参考代码,它开始变得有点长。

    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<T> 允许 foreach 实施。
    • 您可能还想实现 IList<T>
        4
  •  4
  •   ram    16 年前

    通常,出于性能原因,集合不是线程安全的,哈希表除外。您必须使用IsSynchronized和SyncRoot来确保线程安全。看到了吗 here here

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

    编辑:如果您使用的是.net 4.0,则可以使用 concurrent collections

        5
  •  3
  •   Donnie    16 年前

    lock() 除非你有别的理由复印。只有在以不同顺序请求多个锁时,才会发生死锁,例如:

    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

    如果你必须用多把锁,一定要用同样的顺序。如果你只拿了一把锁,就不会造成死锁。。。除非您在等待用户输入时持有一个锁,但从技术上讲,这不是一个死锁,并导致另一点:永远不要持有一个锁超过您绝对必须的时间。

    推荐文章