繁体   English   中英

为什么这种双重检查锁定正确? (。净)

[英]Why is this double-checked locking correct? (.NET)

我已经阅读了很多关于双重检查锁定的危险,我会努力远离它,但据说说我认为它们是一个非常有趣的阅读。

我正在阅读Joe Duffy关于使用双重检查锁定实现单例的这篇文章: http//www.bluebytesoftware.com/blog/PermaLink,guid,543d89ad-8d57-4a51-b7c9-a821e3992bf6.aspx

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

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。

我认为关键在于链接文章( http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S5 )。 具体来说,MS实现的.NET 2.0内存模型具有以下属性:

写入不能移动到同一线程的其他写入。

Duffy提到在IA-64上做了很多工作来支持这个:

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

请注意,Duffy还提到这是MS特定的保证 - 它不是ECMA规范的一部分(至少在2006年的文章撰写时)。 所以,Mono可能不那么好。

初步评论

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

值类型显然不能存储null值,因此必须使用另一个变量来表示初始化的完成。 作者提到所有这些,然后混淆地讨论将instance读为null 据推测,作者想到的是一个非常幼稚的开发人员,他曾一度错误地使用这种变体对值类型,然后继续对参考类型应用它,也不正确。 在值类型的情况下,线程可以读取并使用具有默认字段初始化的struct ,如果不是这样的话。 在引用类型的情况下,线程可以读取并使用null实例。

Thread.VolatileRead的使用是作者提出的修复此变体的建议。 如果没有volatile读取,则返回语句中的instance读取可能会在读取initialized之前解除。

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;
    }
  }
}

希望上面对代码的重新排序清楚地说明了这个问题。 并且显而易见的是, initialized的易失性读取应该防止instance的读取被解除。

而且,我认为作者只是展示了一种可能的方法来解决这种特殊的变化而不是作者一般都提倡这种方法。

回答你的问题

我的问题是,是否仍然存在写入重新排序的危险?

YES(限定):正确指出对instance的写入和initialized可以在lock内部交换。 更糟糕的是,这可能内部正在进行的写入Singleton.ctor也可以以这样的方式不按顺序发生该instance被指定的实例完全初始化 另一个线程可以看到instance集,但该实例可能处于部分构造状态。

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

作者使用Thread.VolatileRead来“修复”这种变化一般不会起作用,因为它没有解决重新排序写入的问题。 代码不是100%可移植的。 这就是为什么我怀疑作者提出这种变化的原因之一。

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

根据http://msdn.microsoft.com/en-us/library/ee817670.aspx一样的单身人士

// .NET Singleton
sealed class Singleton 
{
    private Singleton() {}
    public static readonly Singleton Instance = new Singleton();
}

保证是线程安全的

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM