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

Bry*_*end 22 c# foreach multithreading locking toarray

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

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

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

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

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

Han*_*ant 43

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

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

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

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

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


Lar*_*mie 11

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

您可以在此处找到Thread-Safe字典的实现,以帮助您入门.

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

  • ReaderWriterLockSlim是一个很好的选择.它允许多个线程同时枚举集合,这与只允许一个线程的简单锁定不同.它也不会产生ToArray()所涉及的开销. (2认同)

Rob*_*son 5

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

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

例如,我整理了以下内容来说明这个想法。我很抱歉,它绝不是参考代码,并且开始变得有点长。

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();
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 我包括IEnumerable<T>允许foreach实施。
  • 您提到您担心创建新列表的时空性能,因此这可能对您不起作用。
  • 您可能还想实施 IList<T>