锁定与ToArray的线程安全foreach访问List集合

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

msdn的例子

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

编辑:如果您使用.net 4.0,则可以使用并发集合

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

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