根据我的理解,给定一个 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
循环,这对于Array
和List<T>
以及几乎每个由Microsoft编写的集合都是线程安全的。特别是在您拥有的示例中:
var temp = new List<int>();
foreach (var name in Names)
{
temp.Add(name.Length * 2);
}
您可以根据需要跨任意数量的线程执行此操作。他们都会高兴地从Names
中读取相同的值。
如果您从另一个线程写信给它(这不是您的问题,但值得注意)
使用for
循环遍历Array
或List<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
分配给局部变量,并将其用于代码的其余部分。这有效地将结构中的一个字段复制到局部变量,以便它与其他线程隔离。