13

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

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

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

aList.Remove(Element);

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

谢谢!

4

6 回答 6

16

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

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

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

于 2012-04-04T19:19:54.080 回答
12

方法#1:

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

// 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();
}
于 2012-04-04T20:01:06.430 回答
11

线程 A:

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

线程 B:

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

这当然对性能非常不利。

于 2012-04-04T19:19:25.533 回答
1

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

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

于 2012-04-04T19:36:22.363 回答
0

如果您只想避免使用异常

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

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

于 2012-04-04T19:31:40.287 回答
0

我无法从您的问题中具体看出,但是(看起来)您正在对每个项目执行操作,然后将其删除。您可能想查看BlockingCollection<T>,它有一个调用方法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);
    }
}
于 2012-04-04T19:33:37.800 回答