关于多线程:在迭代时从另一个线程修改列表c#

Modifying list from another thread while iterating (C#)

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

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

然而,在另一条线索中,我称之为

1
aList.Remove(Element);

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

谢谢!


What is the best way to handle this (I would perfer it to be rather simple even at the cost of performance)?

基本上:不要试图在不锁定的情况下修改来自多个线程的非线程安全集合。你正在迭代的事实在这里基本上是不相关的——它只是帮助你更快地找到它。两个线程同时调用Remove是不安全的。

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

  • 没想过,但现在你说这有道理了。谢谢,我以后可能会用到这个,但对于这个例子来说,锁可以正常工作。


方法1:

最简单和最低效的方法是为读者和作者创建一个关键部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Writer
lock (aList)
{
  aList.Remove(item);
}

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

方法2:

这与方法1类似,但在foreach循环的整个持续时间内,您不必持有锁,而是先复制集合,然后在副本上迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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应该是类的内部对象,否则会发生死锁。
  • @德瑞克:是的,这是一个很好的做法,而且很有名。实际上,不太可能真正导致死锁,因为此类事件(嵌套锁等)的典型场景在此不起作用。因此,除非另一段代码完全滥用aList,否则可能发生的最坏情况是增加锁争用。我认为整个江户方面的辩论有点过分了,而且有点过于偏执,有些人不会承认。不过,避免使用这种成语仍然是一种很好的做法。
  • 我还应该指出,在我思考这件事的时候,3需要完全按照介绍的方式来执行。任何偏差(例如决定是否可以分配给local)都可能导致随机的、惊人的失败。据我所知,它甚至可能在时空中撕裂一个整体。


线程A:

1
2
3
4
5
lock (aList) {
  foreach (Type name in aList) {
     name.doSomething();
  }
}

线程B:

1
2
3
lock (aList) {
  aList.Remove(Element);
}

当然,这对性能真的很不利。

  • 谢谢,工作得很有魅力:)希望我用它不会有任何性能问题。


如果有多个读卡器,请尝试读写器锁(.net 3.5+),Slim:http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

如果您只有一个读卡器,那么列表本身或私有对象(但不要锁定类型本身),如EugenRieck的答案所示。


我不能从你的问题中明确地分辨出来,但是(看起来)你正在对每个项目执行一个操作,然后删除它。您可能需要查看BlockingCollection,它有一个调用GetConsumingEnumerable()的方法来查看它是否适合您。这是一个小样本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
    }
}

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

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

意识到剂量学()在另一个线程中删除元素的情况下也执行

  • 不幸的是,ToArrayRemove可能会出现种族差异,从而导致不同类型的例外。不过,这是个好主意!请参阅我的答案,了解如何使其正确工作。
  • 哦……欢迎来到StackOverflow:)