Lock vs. ToArray用于List集合的线程安全foreach访问

时间:2010-06-27 21:02:57

标签: c# multithreading foreach locking toarray

我有一个List集合,我想在多线程应用程序中迭代它。我需要在每次迭代时保护它,因为它可以被更改,而且当我做foreach时我不希望“收集被修改”异常。

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

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

  2. 每次执行foreach时,使用List<> .ToArray()复制到数组。这会导致性能下降,但很容易做到。我担心内存颠簸以及复制它的时间。看起来过分了。使用ToArray是否安全?

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

5 个答案:

答案 0 :(得分:43)

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

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

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

  3. 确保将foreach的能力视为礼物,而不是问题。是的,显式的(;;)循环不会抛出异常,它只会出现故障。就像迭代相同的项目两次或完全跳过一个项目。您可以避免通过向后迭代来重新检查项目数。只要其他线程只调用Add()而不是Remove(),其行为与ToArray()类似,您将获得过时的视图。并非这在实践中有效,索引列表也不是线程安全的。列表与LT;>如有必要,将重新分配其内部数组。这不会以不可预测的方式发挥作用和失灵。

  4. 这里有两点观点。你可能会害怕并遵循共同的智慧,你会得到一个有效但可能不是最佳的程序。这是明智的,让老板高兴。或者你可以试验并亲自了解规则如何使你陷入困境。哪个会让你开心,你会成为一个更好的程序员。但是你的生产力会受到影响。我不知道你的日程安排是什么样的。

答案 1 :(得分:11)

如果您的列表数据大部分是只读的,您可以使用ReaderWriterLockSlim

允许多个线程同时安全地访问它

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

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

答案 2 :(得分:5)

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

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

e.g。我掀起了以下内容来证明这个想法。我会道歉它绝不是参考代码,它开始变得有点长。

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>

答案 3 :(得分: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

答案 4 :(得分:3)

除非您有其他理由进行复制,否则请使用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并且两者都不能继续直到另一个释放其锁定。

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