C# 为什么双重检查锁定正确?(.NET)

C# 为什么双重检查锁定正确?(.NET),c#,concurrency,locking,thread-safety,singleton,C#,Concurrency,Locking,Thread Safety,Singleton,我已经读了很多关于双重检查锁定的危险性的书,我会尽力远离它,但是我认为这是一本非常有趣的书 我正在阅读Joe Duffy的这篇文章,内容是关于使用双重检查锁定实现singleton: 他似乎提出的解决方案(变体)是: class Singleton { private static object slock = new object(); private static Singleton instance; private static int initialized; private Sin

我已经读了很多关于双重检查锁定的危险性的书,我会尽力远离它,但是我认为这是一本非常有趣的书

我正在阅读Joe Duffy的这篇文章,内容是关于使用双重检查锁定实现singleton:

他似乎提出的解决方案(变体)是:

class Singleton {
private static object slock = new object();
private static Singleton instance;
private static int initialized;
private Singleton() {}
public Instance {
    get {
        if (Thread.VolatileRead(ref initialized) == 0) {
            lock (slock) {
                if (initialized == 0) {
                    instance = new Singleton();
                    initialized = 1;
                }
            }
        }
        return instance;
    }
}
}

我的问题是,这是否仍然有被重新排序的危险?具体来说,这两条线:

instance = new Singleton();
initialized = 1;

如果这些写入是反向的,那么其他线程仍然可以读取null。

我认为关键在链接的文章()中。具体而言,MS实现的.NET 2.0内存模型具有以下属性:

写入操作不能超过同一线程中的其他写入操作

Duffy提到在IA-64上为支持这一点做了大量工作:

我们通过st.rel指令确保写操作在IA-64上具有“release”语义来实现这一点。单个st.rel x保证,至少在x的新值对另一个逻辑处理器可见时,每个逻辑处理器都会出现导致其执行(在物理指令流中)的任何其他加载和存储。加载可以被赋予“获取”语义(通过ld.acq指令),这意味着在ld.acq x之后发生的任何其他加载和存储不能出现在加载之前

注意,Duffy还提到这是一个MS特定的保证——它不是ECMA规范的一部分(至少在2006年撰写本文时是这样)。因此,单声道可能没那么好。

根据 单身汉

// .NET Singleton
sealed class Singleton 
{
    private Singleton() {}
    public static readonly Singleton Instance = new Singleton();
}
保证是线程安全的

该框架在内部保证静态类型初始化时的线程安全。[…]在框架本身中,有几个类使用这种类型的单例,尽管使用的属性名称为Value。概念完全相同


初步评论

我不一定认为那篇文章的作者实际上建议使用双重检查锁定模式的这种变体本身。我认为他只是指出,这是一种可能会被天真的开发人员考虑在值类型的上下文中解决问题的变体

值类型显然不能存储
null
值,因此必须使用另一个变量来表示初始化完成。作者提到了所有这些,然后混淆地谈到将
实例
读为
。据推测,作者想到的是一个非常天真的开发人员,他曾在值类型上错误地使用过这种变体,然后继续将其错误地应用于引用类型。在值类型的情况下,线程可以读取并使用默认字段初始化的
struct
。对于引用类型,线程可以读取并使用
null
实例

作者建议使用
Thread.volatireRead
来修复这种变化。没有volatile read,return语句中
实例
的读取可以在
的读取初始化之前解除,如下所示

class Singleton 
{
  private static object slock = new object();
  private static Singleton instance;
  private static int initialized;
  private Singleton() {}

  public Instance {
    get {
        var local = instance;
        if (initialized == 0) {
            lock (slock) {
                if (initialized == 0) {
                    instance = new Singleton();
                    initialized = 1;
                }
            }
        }
        return local;
    }
  }
}
希望上面对代码的重新排序清楚地说明了这个问题。同样明显的是,对
初始化
的易变读取会阻止对
实例
的读取被解除

再一次,我认为作者只是展示了一种可能的方法来修复这个特殊的变化,而不是作者在总体上提倡这种方法

回答您的问题

我的问题是,这难道还不存在被忽视的危险吗 重新排序

是(合格):正如您正确指出的,对
实例
初始化
的写入可以在
内交换。更糟糕的是,
Singleton.ctor
内部可能发生的写入也可能发生无序,导致在实例完全初始化之前分配了
instance
。另一个线程可以看到
实例
集,但该实例可能处于部分构造状态

但是,Microsoft CLI实现中的写入具有发布限制语义。这意味着我刚才所说的一切在任何硬件平台上使用.NET Framework运行时都不适用。但是,像ARM上运行的Mono这样的模糊环境可能会表现出有问题的行为

作者使用
Thread.volatireRead
来“修复”这种变体通常不起作用,因为它无法解决重新排序写入的问题。代码不是100%可移植的。这就是我怀疑作者提出这种变化的原因之一


将单个
实例
变量与
volatile
结合使用的规范变体显然是正确的解决方案。
volatile
关键字在读取时具有获取围栏语义,在写入时具有释放围栏语义,因此它解决了这两个问题;你确定的和文章中提到的那个。

你没有回答这个问题。哦,我明白了。我认为这两条线路是安全的(由它们自己,在一个单身汉身上有一把合适的锁来保护)。但是,
初始化
的易失性/非易失性检查似乎很脆弱。这就是你想要的吗?你的答案是关于静态构造函数是线程安全的。问题是关于双重检查锁定的特定方法。完全不同的话题-1请发布有关双重检查锁定“危险”的链接。我所知道的唯一危险是没有正确地实现它。另外,您使用的是什么版本的.NET?如果您使用的是.NET4.0,那么您可以使用
Lazy
进行延迟初始化。回答得很好!很抱歉,谢谢你读得比我好