首页 > 解决方案 > Parallel.ForEach 中的 C# List.Find() 有时返回 NULL

问题描述

使用以下代码,通常一切正常。但是,很少(可能每月一次,当代码每天运行时)该.Find()方法找不到它正在寻找的项目。

List<string> names = new List<string>();
names.Add("bob");
names.Add("henry");
names.Add("mary");

List<Person> peopleList = GetListOfPeople();

Parallel.ForEach(names, (name) => {
    Person personFound = peopleList.Find(p => p.Name.Equals(name));
    int varThatCantBeNULL = personFound.Age;
});

偶尔peopleList.Find()会返回一个对象,但它的字段为 NULL。我怎么知道这个?

  1. 重新运行应用程序不会再次出现问题(输入或状态不会改变会改变 peopleList 的内容)
  2. 添加代码以在问题发生时打印 peopleList 的内容表明列表中存在满足传递给的条件的项目Find()并且其字段不为 NULL

我无法重现问题,必须等待它有机/自然地发生。

我假设这与并行性和多个线程试图从同一个列表中读取有关。我已经通过捕获错误并重新尝试来解决这个问题,这似乎有效。

谁能提供有关为什么会发生这种情况的见解?我知道从多个线程写入一个公共变量是有问题的(竞争条件),但我不明白为什么从一个公共列表中读取会导致问题。

编辑

感谢您的回复。所有似乎都表明我的使用personFound不是线程安全的,因为其他线程正在写入它(确实有代码写入我试图访问的字段并遇到问题)。

但是,这个线程中的这个答案Can I use local variables inside Parallel Foreach 循环(不会无意中重写以前的值) 表明我正在做的事情是线程安全的。好像有点矛盾。它确实说 ADO.NET 类不是线程安全的,我使用的是自定义类而不是 ADO.NET 类。

标签: c#parallel-processingparallel.foreach

解决方案


既然您说您在阅读时从不写该列表 - 那么阅读本身不会引起任何问题。当只有读取器而根本没有写入器时,所有结构都是线程安全的,这就是不可变结构如此受欢迎的原因——您无法修改它们,因此它们在设计上是线程安全的。

但是,您正在修改Person存储在列表中的条目字段,同时从其他线程读取这些字段。这当然不安全。

看来你相信

Person personFound = peopleList.Find(p => p.Name.Equals(name));

以某种方式创建一个副本,并且保证对字段的后续访问personFound返回与您找到它时相同的值。不是这样的,如果Person是引用类型(我们可以安全地假设它是)。

.NET 中有两种不同的“类型”:引用类型和值类型。引用类型的变量存储一个地址,通过该地址,您可以到达存储在其他地方的实际内容。

在您的情况下 -personFound变量确实是“本地的”,但它只包含实际内容的地址,并且提到的内容被其他线程更改。所以当你这样做时:

int varThatCantBeNULL = personFound.Age;

它粗略的意思是 - 转到personFound存储内容的位置并从那里抓取Age。在此刻:

Person personFound = peopleList.Find(p => p.Name.Equals(name));

该 Age 可能不为空,但在当前线程有机会读取Age下一行之前 - 另一个线程可能已经修改了它。

在您在编辑中链接的问题中 - 情况不一样,因为它们有

 string strType = drow["type"]

虽然字符串也是引用类型 - 它是不可变的并且在创建后无法更改(“修改”字符串的方法只是创建一个副本,修改该副本并返回它)。因此,如果strType其他线程无法更改内容。


推荐阅读