为什么(或不是)在构造函数中设置字段是否安全?

Cha*_*ion 13 c# multithreading volatile memory-model

假设你有一个像这样的简单类:

class MyClass
{
    private readonly int a;
    private int b;

    public MyClass(int a, int b) { this.a = a; this.b = b; }

    public int A { get { return a; } }
    public int B { get { return b; } }
}
Run Code Online (Sandbox Code Playgroud)

我可以以多线程方式使用这个类:

MyClass value = null;
Task.Run(() => {
    while (true) { value = new MyClass(1, 1); Thread.Sleep(10); }
});
while (true)
{
    MyClass result = value;
    if (result != null && (result.A != 1 || result.B != 1)) { 
        throw new Exception(); 
    }
    Thread.Sleep(10);
}
Run Code Online (Sandbox Code Playgroud)

我的问题是:我会看到这个(或其他类似的多线程代码)抛出异常吗?我经常看到其他线程可能不会立即看到非易失性写入的事实.因此,似乎这可能会失败,因为写入值字段可能会在写入a和b之前发生.这是可能的,或者内存模型中是否存在使这种(非常常见)模式安全的东西?如果是这样,它是什么?为了这个目的,readonly是否重要?如果a和b是一个无法原子编写的类型(例如自定义结构),这是否重要?

Sri*_*vel 11

编写的代码将从CLR2.0开始工作,因为CLR2.0内存模型保证所有商店都具有发布语义.

释放语义:确保在栅栏后栅栏移动之前没有载荷或存储.之后的说明可能仍然发生在围栏之前.(取自CPOW第512页).

这意味着在分配类引用后无法移动构造函数初始化.

Joe duffy在他关于同一主题的文章中提到了这一点.

规则2:所有商店都有发布语义,即没有加载或商店可能会在一个商店之后移动.

此外,Vance morrison的文章也证实了这一点(章节技巧4:懒惰初始化).

与删除读锁的所有技术一样,图7中的代码依赖于强写入顺序.例如,此代码在ECMA内存模型中是不正确的,除非myValue变为volatile,因为初始化LazyInitClass实例的写入可能会延迟到写入myValue之后,允许GetValue的客户端读取未初始化状态.在.NET Framework 2.0模型中,代码在没有volatile声明的情况下工作.

保证从CLR 2.0开始按顺序写入.它没有在ECMA标准中指定,它只是CLR的微软实现给出了这种保证.如果您在CLR 1.0或CLR的任何其他实现中运行此代码,您的代码可能会中断.

这一变化背后的故事是:(来自CPOW Page 516)

当CLR 2.0被移植到IA64时,它的初始开发发生在X86处理器上,因此它很难处理任意存储重新排序(如IA64所允许的).大多数针对Windows的非Microsoft开发人员编写的目标.NET代码也是如此

结果是,在IA64上运行时,框架中的许多代码都崩溃了,特别是与臭名昭着的双重检查锁定模式有关的代码突然无法正常工作.我们将在本章后面的模式环境中对此进行研究.但总的来说,如果商店可以通过其他商店,请考虑这一点:一个线程可能初始化私有对象的字段,然后在共享位置发布对它的引用; 因为商店可以四处移动,另一个线程可能能够看到对象的引用,读取它,然后在它们仍处于未初始化状态时看到字段.这不仅影响现有代码,还可能违反类型系统属性,例如initonly字段.

因此,CLR架构师决定通过在IA64上发布所有商店作为发布围栏来强化2.0.这为所有CLR程序提供了更强大的内存模型行为.这可以确保程序员不必担心细微的竞争条件,这些条件只会在一个模糊,很少使用和昂贵的架构上体现出来.

注意Joe duffy说他们通过在IA64上发布所有商店作为释放围栏 强化2.0,这并不意味着其他处理器可以重新排序它.其他处理器本身固有地提供了商店(商店后面的商店)不会被重新排序的保证.所以CLR不需要明确保证这一点.