2011-08-26 68 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

優秀的答案。 –