仅在构造函数中使用私有setter是否使对象具有线程安全性?

Sco*_*ock 12 c# immutability thread-safety

我知道我可以创建一个不可变(即线程安全)对象,如下所示:

class CantChangeThis
{
    private readonly int value;

    public CantChangeThis(int value)
    {
        this.value = value;
    }

    public int Value { get { return this.value; } } 
}
Run Code Online (Sandbox Code Playgroud)

但是,我通常会"欺骗"并执行此操作:

class CantChangeThis
{
    public CantChangeThis(int value)
    {
        this.Value = value;
    }

    public int Value { get; private set; } 
}
Run Code Online (Sandbox Code Playgroud)

然后我开始疑惑,"为什么这个有效?" 它真的是线程安全的吗?如果我像这样使用它:

var instance = new CantChangeThis(5);
ThreadPool.QueueUserWorkItem(() => doStuff(instance));
Run Code Online (Sandbox Code Playgroud)

然后它真正做的是(我认为):

  1. 在实例的线程共享堆上分配空间
  2. 初始化堆上实例内的值
  3. 将指向该空间的指针/引用写入局部变量(特定于线程的堆栈)
  4. 将对该线程的引用作为值传递.(有趣的是我编写它的方式,引用是在一个闭包内部,它正在做我的实例正在做的事情,但让我们忽略它.)
  5. 线程进入堆并从实例读取数据.

但是,该实例值存储在共享内存中.这两个线程可能在堆上具有该内存的缓存不一致视图.什么是确保线程池线程实际上看到构造的实例而不是一些垃圾数据?在任何对象构造结束时是否存在隐式记忆障碍?

xan*_*tos 10

  • 将指向该空间的指针/引用写入局部变量(特定于线程的堆栈)
  • 初始化堆上实例内的值

不......颠倒他们.它更类似于:

  • 分配对象的内存
  • 构造函数是(是,基类)调用
  • newoperator/keyword中"返回"对内存/对象的引用,
  • 引用被"保存"在var instance(=赋值运算符)中

您可以通过在构造函数中抛出异常来检查这一点.不会分配参考变量.

通常,您不希望另一个线程能够看到半初始化对象(请注意,在Java的第一个版本中,这不能保证... Java 1.0具有所谓的"弱"内存模型).这是如何获得的?

在英特尔,它保证:

x86-x64处理器不会重新排序两次写入,也不会重新排序两次读取.

这非常重要:-)并且它保证不会发生这个问题.这种保证不是.NET或ECMA C#的一部分,在Intel上,它是由处理器和Itanium(没有这种保证的架构)保证的,这是由JIT编译器完成的(参见相同的链接).似乎在ARM上这不保证(仍然是相同的链接).但我没见过有人说过这件事.

一般来说,在示例中,这并不重要,因为:

几乎所有与线程相关的操作都使用完整的内存屏障(请参阅内存屏障生成器).完整的内存屏障保证屏障之前的所有写入和读取操作都在屏障之前真正执行,屏障之后的所有读取/写入操作都在屏障之后执行.将ThreadPool.QueueUserWorkItem肯定在某个点使用一个完整的内存屏障.并且起始线程必须明确地开始"新鲜",因此它不能有陈旧数据(并且通过/sf/answers/747127951/,我会说可以安全地假设您可以依赖于隐含障碍.)

请注意,英特尔处理器自然是高速缓存一致...如果您不需要,则必须手动禁用缓存一致性(例如,请参阅此问题:https://software.intel.com/en-us/forums/topic/ 278286),所以唯一可能的问题是在寄存器中"缓存"的变量或者预期的读取或延迟写入的变量(这两个"问题"都是通过使用完全"修复"的记忆障碍)

附录

你的两段代码是等价的.自动属性只是一个"隐藏"字段加上样板get/ set分别为return hiddenfield;hiddenfield = value.因此,如果代码的v2出现问题,代码的v1会出现同样的问题:-)