C# List.Find() inside Parallel.ForEach Sometimes Returns NULL
C# List.Find() inside Parallel.ForEach Sometimes Returns 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()
会 return 一个对象,但它的字段为 NULL。我怎么知道的?
- 重新运行使应用程序正常工作,问题不会再次发生(输入或状态的变化不会改变 peopleList 的内容)
- 在出现问题时添加代码打印 peopleList 的内容表明列表中存在满足传递给
Find()
条件的项目,并且它的字段不为 NULL
我无法重现问题,必须等待它发生 organically/naturally。
我假设这与并行性和多个线程试图从同一个列表中读取有关。我已经通过捕获错误并重新尝试来修复它,这似乎有效。
任何人都可以深入了解为什么会发生这种情况吗?我知道从多个线程写入公共变量是有问题的(竞争条件),但我不明白为什么从通用列表会导致问题。
编辑
感谢您的回复。所有这些似乎都表明我对 personFound
的使用不是线程安全的,因为其他线程正在写入它(确实有代码写入我试图访问的字段并且遇到问题)。
但是,这个线程 中的这个答案表明我正在做的是线程安全的。好像有点矛盾。它确实说 ADO.NET classes 不是线程安全的,我使用的是自定义 class 而不是 ADO.NET。
既然你说你从不在阅读时写入该列表 - 那么阅读本身不会造成任何问题。当只有读取器而根本没有写入器时,所有结构都是线程安全的,这就是不可变结构如此流行的原因 - 您无法修改它们,因此它们在设计上是线程安全的。
但是,您正在修改列表中存储的 Person
个条目的字段,同时从其他线程读取这些字段。这当然不安全。
看来你相信
Person personFound = peopleList.Find(p => p.Name.Equals(name));
以某种方式创建了一个副本,并且保证对 personFound
字段的后续访问 return 与您找到它时的值相同。事实并非如此,如果 Person
是引用类型(我们可以放心地假设它是)。
.NET 中有两种不同的类型:引用类型和值类型。引用类型的变量存储一个地址,通过该地址您可以到达存储在其他地方的实际内容。
在你的例子中 - personFound
varaible 确实是“本地的”,但是它只包含实际内容的地址,并且提到的内容被其他线程更改。所以当你这样做时:
int varThatCantBeNULL = personFound.Age;
大致意思是 - 前往存储 personFound
内容的位置并从那里获取 Age
。此时:
Person personFound = peopleList.Find(p => p.Name.Equals(name));
Age 可能不为空,但在当前线程有机会读取下一行中的 Age
之前 - 另一个线程可能已经修改了它。
你在编辑中链接的问题 - 情况不一样,因为他们有
string strType = drow["type"]
虽然字符串也是引用类型 - 它是不可变的并且在创建后不能更改(“修改”字符串的方法只是创建一个副本,修改该副本并 return 它)。所以如果 strType
内容不能被其他线程更改。
使用下面的代码,通常一切正常。但是,.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()
会 return 一个对象,但它的字段为 NULL。我怎么知道的?
- 重新运行使应用程序正常工作,问题不会再次发生(输入或状态的变化不会改变 peopleList 的内容)
- 在出现问题时添加代码打印 peopleList 的内容表明列表中存在满足传递给
Find()
条件的项目,并且它的字段不为 NULL
我无法重现问题,必须等待它发生 organically/naturally。
我假设这与并行性和多个线程试图从同一个列表中读取有关。我已经通过捕获错误并重新尝试来修复它,这似乎有效。
任何人都可以深入了解为什么会发生这种情况吗?我知道从多个线程写入公共变量是有问题的(竞争条件),但我不明白为什么从通用列表会导致问题。
编辑
感谢您的回复。所有这些似乎都表明我对 personFound
的使用不是线程安全的,因为其他线程正在写入它(确实有代码写入我试图访问的字段并且遇到问题)。
但是,这个线程
既然你说你从不在阅读时写入该列表 - 那么阅读本身不会造成任何问题。当只有读取器而根本没有写入器时,所有结构都是线程安全的,这就是不可变结构如此流行的原因 - 您无法修改它们,因此它们在设计上是线程安全的。
但是,您正在修改列表中存储的 Person
个条目的字段,同时从其他线程读取这些字段。这当然不安全。
看来你相信
Person personFound = peopleList.Find(p => p.Name.Equals(name));
以某种方式创建了一个副本,并且保证对 personFound
字段的后续访问 return 与您找到它时的值相同。事实并非如此,如果 Person
是引用类型(我们可以放心地假设它是)。
.NET 中有两种不同的类型:引用类型和值类型。引用类型的变量存储一个地址,通过该地址您可以到达存储在其他地方的实际内容。
在你的例子中 - personFound
varaible 确实是“本地的”,但是它只包含实际内容的地址,并且提到的内容被其他线程更改。所以当你这样做时:
int varThatCantBeNULL = personFound.Age;
大致意思是 - 前往存储 personFound
内容的位置并从那里获取 Age
。此时:
Person personFound = peopleList.Find(p => p.Name.Equals(name));
Age 可能不为空,但在当前线程有机会读取下一行中的 Age
之前 - 另一个线程可能已经修改了它。
你在编辑中链接的问题 - 情况不一样,因为他们有
string strType = drow["type"]
虽然字符串也是引用类型 - 它是不可变的并且在创建后不能更改(“修改”字符串的方法只是创建一个副本,修改该副本并 return 它)。所以如果 strType
内容不能被其他线程更改。