5

所以我見過很多文章聲明在C++中雙重檢查鎖定,通常用於防止多線程嘗試初始化一個懶惰創建的單例,已被打破。普通雙檢查鎖定代碼讀取這樣的:這個修復程序對於雙重檢查鎖定有什麼問題?

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!instance) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 
     } 

     return *instance; 
    } 
}; 

的問題顯然是行分配實例 - 編譯器可以自由分配的對象,然後將指針分配給它,或者設定指針的地方將被分配,然後分配它。後一種情況會破壞成語 - 一個線程可能會分配內存並分配指針,但在其進入睡眠狀態之前不會運行單例的構造函數 - 然後第二個線程將看到該實例不爲null並嘗試返回它,儘管它還沒有建成。

saw a suggestion使用線程本地布爾值,並檢查,而不是instance。事情是這樣的:

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 
    static boost::thread_specific_ptr<int> _sync_check; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!_sync_check.get()) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      // Any non-null value would work, we're really just using it as a 
      // thread specific bool. 
      _sync_check = reinterpret_cast<int*>(1); 
     } 

     return *instance; 
    } 
}; 

這樣每個線程結束了,如果實例已經被創建一次檢查,但在此之後停止,這需要一定的性能損失,但仍沒有那麼糟糕,因爲每次調用鎖定。但是,如果我們只是使用本地靜態布爾呢?:

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static bool sync_check = false; 
     static singleton* instance; 

     if(!sync_check) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      sync_check = true; 
     } 

     return *instance; 
    } 
}; 

爲什麼不能這樣工作?即使sync_check被另一個線程在另一個線程中分配時讀取,垃圾值仍然不爲零,因此也是如此。 This Dr. Dobb's article聲稱你必須鎖定,因爲你永遠不會因爲重新排序指令而與編譯器爭鬥。這讓我覺得這不應該出於某種原因,但我不明白爲什麼。如果序列點的要求像Dobb博士的文章讓我相信的那樣丟失,我不明白爲什麼鎖之後的任何代碼都不能重新排序爲在鎖之前。這將使C++多線程斷開期間。

我想我可以看到編譯器被允許特別重新排序sync_check在鎖之前,因爲它是一個局部變量(即使它是靜態的,我們沒有返回一個引用或指針) - 但是這樣仍然可以通過使其成爲靜態成員(有效全局)來解決。

那麼這項工作還是不會呢?爲什麼?

+2

問題是變量可能在構造函數運行(或完成)之前分配,而不是在分配對象之前分配。 – kdgregory 2009-06-03 14:53:41

+0

謝謝,糾正。我完全誤解了比賽的狀況。 – 2009-06-03 15:21:57

+1

是的,你是對的,現在的C++確實是「多線程斷點」。只考慮標準。編譯器供應商通常會提供解決方法,因此實際結果並不那麼糟糕。 – Suma 2009-06-16 15:58:27

回答

5

您修復不能解決任何事情,因爲在寫入sync_check和實例都可以做出來的順序在CPU上。舉個例子,前兩個實例調用大概同時發生在兩個不同的CPU上。第一個線程將獲取鎖,初始化指針並將sync_check設置爲true,但處理器可能會改變寫入內存的順序。在另一個CPU上,第二個線程可能會檢查sync_check,看看它是否爲真,但實例可能尚未寫入內存。詳細信息請參見Lockless Programming Considerations for Xbox 360 and Microsoft Windows

你提到的應該工作,那麼線程特定sync_check解決方案(假設你初始化指針爲0)。

+0

關於你的最後一句話:是的,但我不確定,但我認爲thread_specific_ptr在內部使用互斥鎖。那麼使用這個解決方案與只是總是鎖定互斥體(沒有雙重鎖定)有什麼關係呢? – n1ckp 2010-06-09 19:28:38

1

有一個關於這一些偉大的閱讀(雖然它的.NET/C#面向)位置:http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

什麼它歸結爲是,你需要能夠告訴它不能重新排序CPU的讀/寫對於這種可變訪問(自從原來的Pentium以來,如果CPU認爲邏輯不受影響,CPU可以對某些指令進行重新排序),並且需要確保緩存一致(不要忘記 - 我們的開發者假設所有內存只是一個平面資源,但實際上,每個CPU內核都有緩存,一些未共享(L1),有些可能有時會共享(L2)) - 您的初始化可能會寫入主RAM,但另一個內核緩存中可能有未初始化的值。如果你沒有任何併發​​語義,CPU可能不知道它的緩存是髒的。

我不知道C++方面,但在.net中,你會指定變量爲volatile以保護對它的訪問(或者你可以使用System.Threading中的內存讀/寫屏障方法)。另外,我已經閱讀.net 2.0,雙重檢查鎖定保證沒有「易變」變量(對於任何.net讀者) - 這不會幫助你與你的C++碼。

如果你想安全,你需要做標記一個變量在C#中揮發性的C++等價的。

+1

C++變量可以被聲明爲volatile,但我懷疑它具有與C#完全相同的語義。我還記得在某處讀到這是一種濫用波動的情況,但我不記得爲什麼我不能判斷這篇文章是如何理由的。 – 2009-06-03 15:57:57

+0

在不同的語言中,這可能是一種濫用(甚至可能是濫用C#)。編寫低鎖或無鎖代碼的一個非常困難的方面是指導上的差異。我已經花時間閱讀了這篇文章,而且似乎即使在微軟內部,一些博客似乎在需要內存圍欄時以及何時使用易失性內容方面與另一方矛盾。可以肯定的是,這是一個難題。 – JMarsch 2009-06-03 16:06:09

+0

在當前的C++中沒有相當於.NET的volatile(按標準定義)。這是C++ 0x標準即將出臺的領域之一。同時你需要使用你的編譯器提供的東西(在Visual Studio中是指volatile和內存圍欄)。 – Suma 2009-06-16 15:56:00

0

「後一種情況打破了習慣用語 - 兩個線程可能最終創建單身人士。」

但是,如果我正確理解了代碼,第一個示例中,您檢查實例是否已經存在(可能會同時由多個線程執行),如果它沒有一個線程被鎖定,實例 - 當時只有一個線程可以執行創建。所有其他線程被鎖定並等待。

一旦創建了實例,並互斥被解鎖下一個等待線程將鎖定互斥體,但它不會嘗試創建新實例,因爲該檢查將失敗。

下一次實例變量被選中時,它將被設置,所以沒有線程會嘗試創建新的實例。

我不知道在哪裏,而另一個線程檢查同一變量一個線程分配新的實例指針實例的情況下 - 但我相信它會正確地在這種情況下進行處理。

我在這裏錯過了什麼嗎?

好了不知道操作的重新排序,但在這種情況下,它會改變邏輯,所以我不希望這樣的情況發生 - 但我對這個話題沒有專家。

+0

你是對的 - 我對實際的競爭條件錯了。問題是第二個線程可能會看到實例非空,並嘗試在第一個線程構建它之前返回它。我編輯了我的帖子。 – 2009-06-03 15:22:53