在 C# 中,使用 for 循环迭代数组是线程安全操作吗?<T> 使用 foreach 循环迭代 IEnumerable 怎么样?



根据我的理解,给定一个 C# 数组,从多个线程并发迭代数组的行为线程安全操作。

通过迭代数组,我的意思是通过一个普通的旧for循环读取数组内的所有位置。每个线程只是读取数组内内存位置的内容,没有人在写入任何内容,因此所有线程都以一致的方式读取相同的内容。

这是一段代码,执行我上面写的内容:

public class UselessService 
{
private static readonly string[] Names = new [] { "bob", "alice" };
public List<int> DoSomethingUseless()
{
var temp = new List<int>();
for (int i = 0; i < Names.Length; i++) 
{
temp.Add(Names[i].Length * 2);
}
return temp;
}
}

因此,我的理解是DoSomethingUseless方法是线程安全的并且无需string[]替换为线程安全类型(例如ImmutableArray<string>)。

我说的对吗?

现在让我们假设我们有一个IEnumerable<T>的实例。我们不知道底层对象是什么,我们只知道我们有一个对象实现IEnumerable<T>,因此我们能够使用foreach循环对其进行迭代。

根据我的理解,在这种情况下,不能保证从多个线程并发迭代此对象是线程安全操作。换句话说,完全有可能同时从不同线程迭代IEnumerable<T>实例会破坏对象的内部状态,从而使对象损坏。

我在这一点上是对的吗?

Array类的IEnumerable<T>实现情况如何?线程安全吗?

换句话说,以下代码线程安全吗?(这与上面的代码完全相同,但现在数组是使用foreach循环而不是for循环迭代的)

public class UselessService 
{
private static readonly string[] Names = new [] { "bob", "alice" };
public List<int> DoSomethingUseless()
{
var temp = new List<int>();
foreach (var name in Names) 
{
temp.Add(name.Length * 2);
}
return temp;
}
}

是否有任何参考说明 .NET 基类库中的哪些IEnumerable<T>实现是 实际上线程安全?

在 C# 中,使用 for 循环迭代数组是线程安全操作吗?

如果您严格谈论从多个线程读取,那么无论您使用的是for还是foreach循环,这对于ArrayList<T>以及几乎每个由Microsoft编写的集合都是线程安全的。特别是在您拥有的示例中:

var temp = new List<int>();
foreach (var name in Names)
{
temp.Add(name.Length * 2);
}

您可以根据需要跨任意数量的线程执行此操作。他们都会高兴地从Names中读取相同的值。

如果您从另一个线程写信给它(这不是您的问题,但值得注意)

使用for循环遍历ArrayList<T>,它只会继续读取,并且当您遇到更改的值时,它会很高兴地读取它们。

使用foreach循环进行迭代,然后取决于实现。如果Array中的值在foreach循环中途更改,它只会继续枚举并为您提供更改的值。

对于List<T>,这取决于您认为的"线程安全"是什么。如果您更关心读取准确的数据,那么它是"安全的",因为它会在枚举中抛出异常并告诉您集合已更改。但是,如果您认为抛出异常不安全,那么它就不安全。

但值得注意的是,这是List<T>中的设计决策,有代码显式查找更改并抛出异常。设计决策将我们带到了下一点:

我们是否可以假设实现IEnumerable的每个集合都可以安全地跨多个线程读取?

在大多数情况下,可以,但不能保证线程安全读取。原因是因为每个IEnumerable都需要实现IEnumerator,它决定如何遍历集合中的项。就像任何类一样,你可以在其中做任何你想做的事情,包括非线程安全的事情,比如:

  • 使用静态变量
  • 使用共享缓存读取值
  • 不努力处理集合在枚举过程中更改的情况
  • 等。

您甚至可以做一些奇怪的事情GetEnumerator()例如每次调用枚举器时都返回枚举器的相同实例。这确实会产生一些不可预测的结果。

我认为如果某些东西可能导致不可预测的结果,则线程不安全。这些事情中的任何一个都可能导致不可预测的结果。

您可以看到List<T>使用的Enumerator的源代码,因此您可以看到它不会做任何奇怪的事情,这告诉您从多个线程枚举List<T>是安全的。

断言您的代码是线程安全的意味着我们必须理所当然地认为UselessService中没有代码会尝试同时替换Names数组的内容"tom" and "jerry"或(更险恶的)null and null。另一方面,使用ImmutableArray<string>可以保证代码是线程安全的,并且每个人都可以通过查看静态只读字段的类型来确保这一点,而无需仔细检查代码的其余部分。

您可能会从ImmutableArray<T>的源代码中找到有趣的这些注释,关于此结构的一些实现细节:

具有 O(1) 可索引查找时间的只读数组。

此类型具有记录的协定,其大小正好是一个引用类型字段。我们自己的System.Collections.Immutable.ImmutableInterlocked类依赖于它,也依赖于外部的其他类。

给维护者和审阅者的重要通知:

此类型应该是线程安全的。作为结构,当其成员在其他线程上执行时,它无法保护自己的字段不从一个线程更改,因为结构只需重新分配包含此结构的字段即可就地更改。因此,每个成员只应取消引用this一次,这一点非常重要。如果成员需要引用数组字段,则计为取消引用this。调用其他实例成员(属性或方法)也算作取消引用this。任何需要多次使用this的成员都必须改为将this分配给局部变量,并将其用于代码的其余部分。这有效地将结构中的一个字段复制到局部变量,以便它与其他线程隔离。

最新更新