2011-08-26 64 views
10

我最近遇到了the following post on the Resharper website。这是双重检查锁定的讨论,并且有以下代码:双重锁定内存模型保证

public class Foo 
{ 
    private static volatile Foo instance; 
    private static readonly object padlock = new object(); 

    public static Foo GetValue() 
    { 
     if (instance == null) 
     { 
      lock (padlock) 
      { 
       if (instance == null) 
       { 
        instance = new Foo(); 
        instance.Init(); 
       } 
      } 
     } 
     return instance; 
    } 

    private void Init() 
    { 
     ... 
    } 
} 

经后使得声称

如果我们假设的init()是用于并初始化的方法状态 美孚,然后上述代码可能无法按预期方式运行,由于 内存模型不保证读取和写入的顺序。作为 结果,对Init()的调用实际上可能在变量 实例处于一致状态之前发生。

这里是我的问题:

  1. 这是我的理解是.NET的内存模型(自2.0至少)有要求instance声明为volatile,因为lock将提供全记忆围栏。情况不是这样,还是我误解了?

  2. 是不是读取/写入重新排序只能观察多个线程?这是我的理解,在一个单一的线程,副作用将是一致的顺序,并且lock就位会阻止任何其他线程观察到的东西是不对的。我是否也在这里?

+2

你对.NET 2.0内存模型是正确的。你不需要'易变'(因为它几乎不会达到你期望的效果),而'锁'的确可以做到完全的篱笆。但是,正如Chibacity指出的那样,当谈到线程安全性时,很容易忽略竞争条件。 – Steven

回答

18

大问题的例子是,第一个空支票没有被锁定,所以实例可能不能为null,但之前初始化被调用。这可能会导致线程在调用Init之前使用实例。

正确的版本因此应:

public static Foo GetValue() 
{ 
    if (instance == null) 
    { 
     lock (padlock) 
     { 
      if (instance == null) 
      { 
       var foo = new Foo(); 
       foo.Init(); 
       instance = foo; 
      } 
     } 
    } 

    return instance; 
} 
+2

这非常尖锐。我错过了我自己。您不要将完整版的代码的正确版本添加到您的答案中,是吗? – Steven

+2

@Steven正确。欢呼编辑 - 赞赏。从我的手机非常困难! :) –

+3

我应该得到额外的积分来回答这个手机:-) – Steven

1

如果我读码正确,问题是:

呼叫者开始方法,发现实例== NULL是真实的,进入锁定,发现实例为STILL为空,并创建实例。

在Init()被调用之前,调用者1的线程被挂起,调用者2进入该方法。调用者2发现实例不为空,并在调用者1可以初始化它之前继续使用它。

0

一方面,它创造了一个“全栅栏”但该帖指的就是那张在“双重检查锁定的情况下”“那里面篱笆” ......看到一个解释http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx

它指出:

However, we have to assume that a series of stores have taken place during construction 
of ‘a’. Those stores can be arbitrarily reordered, including the possibility of delaying 
them until after the publishing store which assigns the new object to ‘a’. At that point, 
there is a small window before the store.release implied by leaving the lock. Inside that 
window, other CPUs can navigate through the reference ‘a’ and see a partially constructed 
instance. 

更换a在上面的句子与instance从您例如...

另外检查了这http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspx出来 - 它解释您的场景中volatile的成就...

围墙和volatile和一个很好的解释了如何volatile具有取决于你运行看http://www.albahari.com/threading/part4.aspx,甚至更多/代码的处理器上,甚至不同的效果更好的信息,请参阅http://csharpindepth.com/Articles/General/Singleton.aspx

12

这是我的理解是.NET存储器模型(自2.0至少) 已经不需要该实例被声明为易失性的,因为锁 将提供一个完整的存储器围栏。是不是这种情况,或者是我误导了我 ?

这是必需的。原因是因为您正在访问lock以外的instance。让我们假设你省略了volatile并且已经解决了这样的初始化问题。

public class Foo 
{ 
    private static Foo instance; 
    private static readonly object padlock = new object(); 

    public static Foo GetValue() 
    { 
     if (instance == null) 
     { 
      lock (padlock) 
      { 
       if (instance == null) 
       { 
        var temp = new Foo(); 
        temp.Init(); 
        instance = temp; 
       } 
      } 
     } 
     return instance; 
    } 

    private void Init() { /* ... */ } 
} 

在某种程度上C#编译器,JIT编译器,或硬件可以发出,优化走temp可变,并且使获得分配instance变量之前Init是RAN的指令序列。事实上,它甚至可以在构造函数运行之前分配instanceInit方法使问题更容易发现,但问题仍然存在于构造函数中。

这是一个有效的优化,因为指令自由地锁内重新排序。甲lock确实发射存储器障碍,但仅在Monitor.EnterMonitor.Exit呼叫。

现在,如果您确实省略了volatile,则该代码可能仍然适用于大多数硬件和CLI实施组合。原因是x86硬件的内存模型更加紧凑,微软CLR的实现也非常紧张。但是,关于此主题的ECMA规范相对较宽松,这意味着CLI的另一个实现可自由进行微软当前选择忽略的优化。您必须编写可能是CLI抖动的较弱模型,而不是大多数人倾向于关注的硬件。这就是为什么volatile仍然是必需的。

没有被读/写重排只观察到相对于多线程 ?这是我的理解是在单个线程,一边 影响将是一致的顺序,并在地方 锁将阻止任何其它线程观察的东西是不妥。 我是否也在这里?

是的。只有当多个线程访问相同的内存位置时,指令重新排序才会发挥作用。即使是最弱的软件和硬件内存模型也不允许任何形式的优化,从而改变开发人员在线程上执行代码时的意图。否则,没有程序会正确执行。问题在于其他线程如何观察该线程中发生了什么。其他线程可能会感知与执行线程不同的行为。但是,正在执行的线程总是感觉到正确的行为。不是,lock本身不会阻止其他线程感知不同的事件序列。原因是因为正在执行的线程可能正在执行lock内部的指令,其顺序与开发人员的意图不同。只有在锁的入口和出口处才会产生记忆障碍。因此,在您的示例中,即使您已经用lock包装了这些指令,甚至在构造函数运行之前,也可以将新对象的引用分配给instance

使用volatile,而另一方面,对如何相比的instance尽管在共同智慧的方法开始时的初始检查lock的行为中的代码有更大的影响。很多人认为主要问题是instance可能没有易失性读取陈旧。情况可能是这样,但更大的问题是,在lock内部没有易失性写入的情况下,另一个线程可能会看到instance引用构造函数尚未运行的实例。 volatile写法解决了这个问题,因为它可以防止编译器在写入instance之后移动构造器代码。这就是为什么volatile仍然是必需的。

+1

优秀的答案。 –