引用赋值在多线程中安全吗?

47

我正在用C#构建一个多线程缓存,它将保存一系列Car对象:

public static IList<Car> Cars {get; private set;}
我想知道在没有锁定的情况下更改线程中的引用是否安全?例如:
private static void Loop()
{
  while (true)
  {
    Cars = GetFreshListFromServer();
    Thread.Sleep(SomeInterval);
  }
}

基本上就是看给Cars分配一个新引用是否具有原子性,我猜。

如果没有,我显然必须为我的汽车使用私有字段,并在获取和设置周围进行锁定。


1
顺便提一下,static 列表容易受到变异问题的影响;不要在锁之外改变它! - Marc Gravell
好主意,我可能最终会使用自定义的“get”并返回一个副本或只读版本。 - Steffen
2
顺便提一下,你考虑过使用ConcurrentDictionary<Car,Car>来增加查找时间O(1)并管理并发吗?另外,你真的应该确保只有一个线程会刷新列表。否则,你将会比所需做更多的网络调用。 - Josh Smeaton
没有考虑过。这似乎非常不错,我可能需要一个 dictionary<int, Car> 来进行查找,所以这可能会很方便。至于刷新,我在静态构造函数中生成线程,因此只会有一个线程。 - Steffen
2个回答

78

是的,在语言规范中,引用更新保证是原子操作。

5.5 变量引用的原子性

以下数据类型的读写是原子性的:bool、char、byte、sbyte、short、ushort、uint、int、float和引用类型。此外,具有前面列表中基础类型的枚举类型的读写也是原子性的。其他类型的读写,包括long、ulong、double和decimal以及用户定义类型,则不能保证是原子的。

然而,在一个紧密的循环内,您可能会受到寄存器缓存的影响。除非您的方法调用被内联(这可能会发生),否则在这种情况下不太可能发生。个人建议添加lock以使其简单和可预测,但volatile也可以在这里提供帮助。请注意,完全的线程安全不仅仅是原子性。

在缓存的情况下,我会自己查看Interlocked.CompareExchange——即尝试进行更新,但如果失败,则重新从头开始(从新值开始)并重试。


+1,非常感谢您的快速回复。您不会碰巧有语言规范的链接吧?如果可以在线获取的话。 - Steffen
@Steffan 只需搜索“C# 语言规范”,很容易找到微软版本。或者搜索 ECMA334 获取 2.0 公共版本。 - Marc Gravell
非常好的阐述,我会看一下 Interlocked.CompareExchange :-) - Steffen
2
有人能详细解释一下“寄存器缓存可能会使你吃亏”吗?这会有什么后果? - Nathan
2
@Nathan 简单来说:线程A不一定能看到线程B对变量所做的更改。这在像 bool shouldRun = true;while(shouldRun) {...} 这样的情况下尤为明显,而第二个线程则将 shouldRun = false; - Marc Gravell
2
截至2018年10月,最新的标准是C# 5,发布于2017年12月,可在https://www.ecma-international.org/publications/files/ECMA-ST/Ecma-334.pdf上获取,相关章节为:10.6 变量引用的原子性。 - tymtam

0
在@Marc Gravell的回答中,参考了C#语言规范5.5,重要的是要知道“用户定义类型”这个术语的含义。我没有在C#语言规范中找到关于此用法的清晰定义。在UML和常用语中,类是类型的实例。但在C#语言规范的上下文中,它的含义不清楚。
Visual Basic语言参考部分“用户定义类型”(位于https://msdn.microsoft.com/en-us/library/cec05s9z.aspx)中说:
“以前版本的Visual Basic支持用户定义类型(UDT)。当前版本将UDT扩展为结构。”
因此,似乎用户定义类型是一个结构,而不是一个类。
但是...
根据“C#编程指南”中的“类型”一节(位于https://msdn.microsoft.com/en-us/library/ms173104.aspx):
一个典型的C#程序使用类库中的类型以及用户定义的类型,这意味着类是一种用户定义的类型。稍后,它举了一个“复杂的用户定义类型”的例子:
MyClass myClass;
这意味着“MyClass”是一种用户定义的类型。然后它说:
“CTS中的每个类型都被定义为值类型或引用类型。这包括.NET Framework类库中的所有自定义类型和您自己定义的类型。”
这意味着开发人员创建的所有类都是“用户定义的类型”。
最后,有一个Stackoverflow条目,在其中讨论了这个术语的含义,但没有得出结论:如何确定C#中的属性是否为用户定义类型? 因此,为了安全起见,我被迫考虑所有类,无论是我创建的还是在.Net Framework中找到的类,都是用户定义类型,因此不适合用于赋值的线程安全,因为C#语言规范第5.5节中说道:

对于...以及用户定义类型的读写操作不能保证原子性。

很遗憾,在C#语言规范这样一个精确的规范中使用了口语化的术语。由于存在这种歧义,为了线程安全,我可能会编写比“用户定义类型”不包括CLR类更少优化的代码。
因此,我要求进一步澄清此stackoverflow答案,因为它目前的基础留下了这个重大的歧义。就目前而言,对问题“引用赋值是否线程安全?”的答案似乎是“否”。

9
尽管“用户定义类型”这个术语的使用似乎存在不一致性,但我认为这并不会对原始答案产生质疑。摘录相关部分:“以下数据类型的读写是原子性的:bool、char、...和引用类型。包括…用户定义类型在内的其他类型的读写不能保证是原子性的。” 在我看来,这种情况下的“其他”清楚地排除了最初列表中涵盖的任何内容(即引用类型)。因此,即使“用户定义类型”有时可能用于描述类,但它在这里肯定不是。 - The Mad Coder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接