我知道我可以像这样创建一个不可变的(即线程安全的)对象:
class CantChangeThis
{
private readonly int value;
public CantChangeThis(int value)
{
this.value = value;
}
public int Value { get { return this.value; } }
}
然而,我通常会“欺骗”并执行此操作:
class CantChangeThis
{
public CantChangeThis(int value)
{
this.Value = value;
}
public int Value { get; private set; }
}
然后我想知道,“为什么这有用?”它真的是线程安全的吗?如果我这样使用它:
var instance = new CantChangeThis(5);
ThreadPool.QueueUserWorkItem(() => doStuff(instance));
然后它真正做的是(我认为):
但是,该实例值存储在共享内存中。这两个线程可能在堆上具有该内存的缓存不一致视图。什么是确保线程池线程实际上看到构造的实例而不是一些垃圾数据?在任何对象构造结束时是否存在隐式内存屏障?
答案 0 :(得分:10)
不......反转他们。它更类似于:
new
运算符/关键字var instance
(=
分配运算符)您可以通过在构造函数中抛出异常来检查这一点。不会分配参考变量。
通常,您不希望另一个线程能够看到半初始化对象(请注意,在Java的第一个版本中,这不能保证... Java 1.0具有所谓的“弱”内存模型)。这是怎么得到的?
在英特尔上,它是guaranteed:
x86-x64处理器不会重新排序两次写入,也不会重新排序两次读取。
这非常重要:-)并且它保证不会发生这个问题。这种保证不是.NET或ECMA C#的一部分,但在英特尔上它是,它是由处理器和Itanium(没有这种保证的架构)保证的,这是由JIT编译器完成的(见相同的链接)。似乎在ARM上这不保证(仍然是相同的链接)。但我没见过有人说过它。
一般来说,在示例中,这并不重要,因为:
几乎所有与线程相关的操作都使用完整的内存屏障(参见Memory barrier generators)。完整的内存屏障保证屏障之前的所有写入和读取操作都在屏障之前真正执行,屏障之后的所有读取/写入操作都在屏障之后执行。 ThreadPool.QueueUserWorkItem
肯定在某一点使用一个完整的记忆障碍。并且起始线程必须明确地开始“新鲜”,因此它不能有陈旧数据(并且https://stackoverflow.com/a/10673256/613130,我认为可以安全地假设您可以依赖隐式障碍。)
请注意,英特尔处理器自然是高速缓存一致的...如果你不想要它,你必须手动禁用缓存一致性(例如参见这个问题:https://software.intel.com/en-us/forums/topic/278286),所以唯一可能的问题是在寄存器中“缓存”的变量,或者预期的读取或延迟写入的变量(这些“问题”都是通过使用完整的内存屏障来“修复”)
<强>附录强>
你的两段代码是等价的。自动属性只是一个“隐藏”字段以及分别为get
和set
的样板return hiddenfield;
/ hiddenfield = value
。因此,如果代码的v2出现问题,代码的v1会出现同样的问题: - )
答案 1 :(得分:0)
如果没有任何东西绕过语言级别块来调用setter(可以用反射完成),那么你的对象将保持不可变和线程安全,就像你使用只读字段一样。
关于共享内存和缓存不一致的视图,这些是由框架,操作系统和硬件处理的细节,因此在编写像这样的高级别的东西时你不必担心它们。 / p>