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

时间:2011-12-02 01:33:54

标签: c# concurrency locking thread-safety singleton

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

我正在阅读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;

如果这些写入被反转,那么其他一些线程仍然可以读取空值。

3 个答案:

答案 0 :(得分:5)

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

  

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

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

  

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

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

答案 1 :(得分:1)

初步评论

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

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

使用Thread.VolatileRead是作者修复此变体的提议。如果没有易失性读取,返回语句中的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的读取被解除。

同样,我认为作者只是展示了一种可能的方法来修复这个特定的变体而不是作者一般都提倡这种方法。

回答您的问题

  

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

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

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

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

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

答案 2 :(得分:-1)

根据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。这个概念完全一样。