【问题标题】:Modifying list from another thread while iterating (C#)迭代时从另一个线程修改列表(C#)
【发布时间】:2012-04-04 19:17:24
【问题描述】:

我正在使用 foreach 循环遍历元素列表,如下所示:

foreach (Type name in aList) {
   name.doSomething();
}

但是,在另一个线程中,我正在调用类似的东西

aList.Remove(Element);

在运行时,这会导致 InvalidOperationException: Collection was modified;枚举操作可能无法执行。处理这个问题的最佳方法是什么(即使以性能为代价,我也希望它相当简单)?

谢谢!

【问题讨论】:

    标签: c# multithreading list thread-safety


    【解决方案1】:

    处理此问题的最佳方法是什么(即使以性能为代价,我也希望它相当简单)?

    从根本上说:不要尝试在没有锁定的情况下从多个线程修改非线程安全的集合。您正在迭代的事实在这里几乎无关紧要 - 它只是帮助您更快地找到它。两个线程同时调用Remove 是不安全的。

    要么使用线程安全的集合,例如ConcurrentBag确保只有一个线程对集合执行任何操作时间。

    【讨论】:

    • 没想到这一点,但现在你说这是有道理的。谢谢,我稍后可能会使用它,但是对于这种情况,锁可以正常工作。
    【解决方案2】:

    方法一:

    最简单但效率最低的方法是为读者和作者创建一个临界区。

    // Writer
    lock (aList)
    {
      aList.Remove(item);
    }
    
    // Reader
    lock (aList)
    {
      foreach (T name in aList)
      {
        name.doSomething();
      }
    }
    

    方法#2:

    这类似于方法 #1,但不是在 foreach 循环的整个持续时间内保持锁定,而是先复制集合,然后遍历副本。

    // Writer
    lock (aList)
    {
      aList.Remove(item);
    }
    
    // Reader
    List<T> copy;
    lock (aList)
    {
      copy = new List<T>(aList);
    }
    foreach (T name in copy)
    {
      name.doSomething();
    }
    

    方法#3:

    这完全取决于您的具体情况,但我通常处理这个问题的方式是保持对集合的主引用不可变。这样,您就不必在阅读器端同步访问。作家方面需要lock。读者方面什么都不需要,这意味着读者保持高度并发。您唯一需要做的就是将aList 引用标记为volatile

    // Variable declaration
    object lockref = new object();
    volatile List<T> aList = new List<T>();
    
    // Writer
    lock (lockref)
    {
      var copy = new List<T>(aList);
      copy.Remove(item);
      aList = copy;
    }
    
    // Reader
    List<T> local = aList;
    foreach (T name in local)
    {
      name.doSomething();
    }
    

    【讨论】:

    • 我喜欢这里的各种解决方案,但在前两个示例中应该注意aList 应该是类的内部对象,否则可能会发生死锁。
    • @DerekW:是的,这是一个很好的做法,并且得到了很好的说明。实际上,它不太可能真正导致死锁,因为这种情况的典型场景(嵌套锁等)在这里没有发挥作用。因此,除非另一段代码完全滥用aList,否则可能发生的最坏情况是锁竞争增加。我认为整个lock(this) 辩论有点夸大其词,并且比某些人承认的偏执狂方面有点过分。同样,避免使用此类习语仍然是一种好习惯。
    • 我还应该指出,当我在考虑它时,#3 需要完全 遵循。任何偏差(比如决定可以省略对local 的分配)都可能导致随机和惊人的失败。据我所知,它甚至可能在时空中撕裂一个整体。
    • 我喜欢解决方案 #3,但只是看不到 volatile 在这里有什么帮助。据我所知,它什么也没做,我们可以跳过它而不会造成任何伤害
    • @Shpand:aList 的读取发生在锁之外,因此为了防止编译器缓存它并确保您获得最新的副本,您必须使用 volatile。
    【解决方案3】:

    线程 A:

    lock (aList) {
      foreach (Type name in aList) {
         name.doSomething();
      }
    }
    

    线程 B:

    lock (aList) {
      aList.Remove(Element);
    }
    

    这当然对性能非常不利。

    【讨论】:

    • 谢谢,就像一个魅力 :) 希望我不会因为使用它而遇到任何性能问题..
    【解决方案4】:

    如果您有多个阅读器,请尝试使用 Reader-Writer Lock (.Net 3.5+),Slim: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

    如果您只有一个阅读器,则只需锁定列表本身或私有对象(但不要锁定类型本身),如 Eugen Rieck 的回答所示。

    【讨论】:

      【解决方案5】:

      如果你只是想避免异常使用

      foreach (Type name in aList.ToArray()) 
      { name.doSomething(); }
      

      注意 做一点事() 如果元素在另一个线程中被删除,也会执行

      【讨论】:

      • 不幸的是,ToArrayRemove 可能会争用可能导致不同类型的异常。不过这是个好主意!请参阅我的answer 以了解如何使其正常工作。
      【解决方案6】:

      我无法从您的问题中具体看出,但是(看起来)您正在对每个项目执行一个操作,然后将其删除。您可能想查看BlockingCollection&lt;T&gt;,它有一个调用GetConsumingEnumerable() 的方法来查看它是否适合您。这是一个小样本。

      void SomeMethod()
      {
          BlockingCollection<int> col = new BlockingCollection<int>();
      
          Task.StartNew( () => { 
      
              for (int j = 0; j < 50; j++)
              {
                  col.Add(j);
              }
      
              col.CompleteAdding(); 
      
           });
      
          foreach (var item in col.GetConsumingEnumerable())
          {
             //item is removed from the collection here, do something
             Console.WriteLine(item);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2017-12-05
        • 2014-09-17
        • 2012-11-05
        • 2012-05-11
        • 1970-01-01
        • 2019-04-04
        相关资源
        最近更新 更多