仅在构造函数中使用私有 setter 是否会使对象线程安全?
Does using private setters only in a constructor make the object thread-safe?
我知道我可以像这样创建一个不可变的(即线程安全的)对象:
class CantChangeThis
{
private readonly int value;
public CantChangeThis(int value)
{
this.value = value;
}
public int Value { get { return this.value; } }
}
但是,我通常 "cheat" 并这样做:
class CantChangeThis
{
public CantChangeThis(int value)
{
this.Value = value;
}
public int Value { get; private set; }
}
然后我想知道,"why does this work?"它真的是线程安全的吗?如果我这样使用它:
var instance = new CantChangeThis(5);
ThreadPool.QueueUserWorkItem(() => doStuff(instance));
那么它真正做的是(我认为):
- 在实例的线程共享堆上分配space
- 正在初始化堆上实例内部的值
- 正在将那个 space 的 pointer/reference 写入局部变量(特定于线程的堆栈)
- 将引用作为值传递给该线程。 (有趣的是,我的写法是,引用位于闭包内,它与我的实例正在做的事情相同,但让我们忽略它。)
- 线程进入堆并从实例中读取数据。
但是,该实例值存储在共享内存中。这两个线程可能对堆上的内存有缓存不一致的看法。是什么确保线程池线程实际看到构造的实例而不是一些垃圾数据?在任何对象构造的末尾是否存在隐式内存屏障?
- 正在将那个 space 的 pointer/reference 写入局部变量(特定于线程的堆栈)
- 正在初始化堆上实例内部的值
不...反转它们。它更类似于:
- 对象的内存已分配
- 构造函数(是,基础 类)被调用
- 对 memory/object 的引用是 "returned" 来自
new
operator/keyword、
var instance
(=
赋值运算符)中的引用是"saved"
您可以通过在构造函数中抛出异常来检查这一点。引用变量不会被赋值。
通常,您不希望另一个线程能够看到半初始化的对象(请注意,在 Java 的第一个版本中,不能保证这一点... Java 1.0有所谓的 "weak" 内存模型)。这是怎么得到的?
在 Intel 上是 guaranteed:
The x86-x64 processor will not reorder two writes, nor will it reorder two reads.
这非常重要 :-) 并且它保证不会发生该问题。此保证不是 .NET 或 ECMA C# 的一部分 但 在 Intel 上它由处理器保证,而在 Itanium(没有该保证的体系结构)上,这是由 JIT 完成的编译器(参见 link)。似乎在 ARM 上不能保证这一点(仍然相同 link)。但是没看到有人说。
一般来说,在例子中给出,这个并不重要,因为:
几乎所有与线程相关的操作都使用完整的内存屏障(参见 Memory barrier generators). A full Memory Barrier guarantees that all write and read operations that are before the barrier are really executed before the barrier, and all the read/write operations that are after the barrier are executed after the barrier. The ThreadPool.QueueUserWorkItem
surely at a certain point uses one full Memory Barrier. And the starting thread must clearly start "fresh", so it can't have stale data (and by 、我认为可以安全地假设您可以依赖隐式屏障。)
请注意,英特尔处理器自然是缓存一致的...如果您不想要它,您必须手动禁用缓存一致性(例如,请参见这个问题:https://software.intel.com/en-us/forums/topic/278286),所以唯一可能的问题是是寄存器中 "cached" 的变量或预期的读取或延迟的写入(并且这两个 "problems" 都是 "fixed" 通过使用完整的内存屏障)
附录
你的两段代码是等价的。自动属性只是一个 "hidden" 字段加上一个样板文件 get
/set
,分别是 return hiddenfield;
和 hiddenfield = value
。所以如果代码的 v2 有问题,代码的 v1 也会有同样的问题:-)
如果没有什么可以绕过语言级块来调用 setter(这可以通过反射完成),那么您的对象将保持不可变和线程安全,就像您使用读取一样-仅字段。
关于共享内存和缓存不一致的视图,这些是由框架、操作系统和您的硬件处理的细节,因此您在编写像这样的高级程序时无需担心它们.
我知道我可以像这样创建一个不可变的(即线程安全的)对象:
class CantChangeThis
{
private readonly int value;
public CantChangeThis(int value)
{
this.value = value;
}
public int Value { get { return this.value; } }
}
但是,我通常 "cheat" 并这样做:
class CantChangeThis
{
public CantChangeThis(int value)
{
this.Value = value;
}
public int Value { get; private set; }
}
然后我想知道,"why does this work?"它真的是线程安全的吗?如果我这样使用它:
var instance = new CantChangeThis(5);
ThreadPool.QueueUserWorkItem(() => doStuff(instance));
那么它真正做的是(我认为):
- 在实例的线程共享堆上分配space
- 正在初始化堆上实例内部的值
- 正在将那个 space 的 pointer/reference 写入局部变量(特定于线程的堆栈)
- 将引用作为值传递给该线程。 (有趣的是,我的写法是,引用位于闭包内,它与我的实例正在做的事情相同,但让我们忽略它。)
- 线程进入堆并从实例中读取数据。
但是,该实例值存储在共享内存中。这两个线程可能对堆上的内存有缓存不一致的看法。是什么确保线程池线程实际看到构造的实例而不是一些垃圾数据?在任何对象构造的末尾是否存在隐式内存屏障?
- 正在将那个 space 的 pointer/reference 写入局部变量(特定于线程的堆栈)
- 正在初始化堆上实例内部的值
不...反转它们。它更类似于:
- 对象的内存已分配
- 构造函数(是,基础 类)被调用
- 对 memory/object 的引用是 "returned" 来自
new
operator/keyword、 var instance
(=
赋值运算符)中的引用是"saved"
您可以通过在构造函数中抛出异常来检查这一点。引用变量不会被赋值。
通常,您不希望另一个线程能够看到半初始化的对象(请注意,在 Java 的第一个版本中,不能保证这一点... Java 1.0有所谓的 "weak" 内存模型)。这是怎么得到的?
在 Intel 上是 guaranteed:
The x86-x64 processor will not reorder two writes, nor will it reorder two reads.
这非常重要 :-) 并且它保证不会发生该问题。此保证不是 .NET 或 ECMA C# 的一部分 但 在 Intel 上它由处理器保证,而在 Itanium(没有该保证的体系结构)上,这是由 JIT 完成的编译器(参见 link)。似乎在 ARM 上不能保证这一点(仍然相同 link)。但是没看到有人说。
一般来说,在例子中给出,这个并不重要,因为:
几乎所有与线程相关的操作都使用完整的内存屏障(参见 Memory barrier generators). A full Memory Barrier guarantees that all write and read operations that are before the barrier are really executed before the barrier, and all the read/write operations that are after the barrier are executed after the barrier. The ThreadPool.QueueUserWorkItem
surely at a certain point uses one full Memory Barrier. And the starting thread must clearly start "fresh", so it can't have stale data (and by 、我认为可以安全地假设您可以依赖隐式屏障。)
请注意,英特尔处理器自然是缓存一致的...如果您不想要它,您必须手动禁用缓存一致性(例如,请参见这个问题:https://software.intel.com/en-us/forums/topic/278286),所以唯一可能的问题是是寄存器中 "cached" 的变量或预期的读取或延迟的写入(并且这两个 "problems" 都是 "fixed" 通过使用完整的内存屏障)
附录
你的两段代码是等价的。自动属性只是一个 "hidden" 字段加上一个样板文件 get
/set
,分别是 return hiddenfield;
和 hiddenfield = value
。所以如果代码的 v2 有问题,代码的 v1 也会有同样的问题:-)
如果没有什么可以绕过语言级块来调用 setter(这可以通过反射完成),那么您的对象将保持不可变和线程安全,就像您使用读取一样-仅字段。
关于共享内存和缓存不一致的视图,这些是由框架、操作系统和您的硬件处理的细节,因此您在编写像这样的高级程序时无需担心它们.