2015-10-14 87 views
2

C++ and the Perils of Double-Checked Locking中,作者舉例說明了如何正確實施模式。在雙重鎖定的鎖定模式中獲取屏障

Singleton* Singleton::instance() { 
    Singleton* tmp = pInstance; 
    ... // insert memory barrier (1) 
    if (tmp == 0) { 
     Lock lock; 
     tmp = pInstance; 
     if (tmp == 0) { 
     tmp = new Singleton; 
     ... // insert memory barrier (2) 
     pInstance = tmp; 
     } 
    } 
    return tmp; 
} 

我無法弄清楚什麼,不過,如果是第一個內存屏障必須Singleton* tmp = pInstance;後? (編輯:要清楚,我知道這個障礙是必要的,我不明白的是它是否必須在分配tmp後纔會出現)如果是這樣,爲什麼?以下內容無效嗎?

Singleton* Singleton::instance() { 
    ... // insert memory barrier (1) 
    if (pInstance == 0) { 
     Lock lock; 
     if (pInstance == 0) { 
     Singleton* tmp = new Singleton; 
     ... // insert memory barrier (2) 
     pInstance = tmp; 
     } 
    } 
    return pInstance; 
} 
+2

我不是在猜測什麼編譯器可能會做一個專家,但即使對正確性並不重要,維護'tmp'確實避免了在普通(已經初始化)的情況下重複讀取全局狀態;如果你的週期非常困難,那麼雙重檢查鎖定似乎是一個好主意,避免重複讀取全局狀態可以確保你不會以其他方式犧牲某些收益; 'tmp'是堆棧本地的(所以共享不是問題);編譯器可以安全地避免重讀它,但可能無法優化第二次直接讀取「pInstance」。 – ShadowRanger

+0

@ShadowRanger所以也許我的修改是有效的,但作者選擇以這種方式實現它爲您提到的優化?不幸的是,這篇論文並沒有解釋最終設計的理由,除了需要設置障礙。 – user1747505

+0

@ user1747505您的更改不能保證將'pInstance'視爲非NULL的線程可以看到底層對象的初始化。您認爲哪種內存屏障可以確保這一點,考慮到將'pInstance'視爲非NULL的線程永遠不會遇到任何內存障礙。 –

回答

2

這很重要。否則,在複製之前,可能會由CPU預取if之後發生的讀取,這將是一場災難。在pInstance不爲NULL並且我們沒有獲取任何鎖的情況下,您必須保證在讀取pInstance之前,在讀取pInstance之後發生的讀取不會重新排序。

考慮:

Singleton* tmp = pInstance; 
if (tmp == 0) { ... } 
return tmp->foo; 

如果CPU讀取tmp->footmp之前會發生什麼?例如,CPU可以將其優化爲:

bool loaded = false; 
int return_value = 0; 

if (pInstance != NULL) 
{ // do the fetch early 
    return_value = pInstance->foo; 
    loaded = true; 
} 

Singleton* tmp = pInstance; 
if (tmp == 0) { ... } 

return loaded ? return_value : tmp->foo; 

注意這是幹什麼用的? tmp->foo的讀取現在已經移到檢查之前,如果指針是非空的。這是CPU可能完成的完全合法的內存預取優化(推測性讀取)。但是對雙重檢查鎖定的邏輯完全是災難性的。

if (tmp == 0)之後的代碼在我們將pInstance視爲非NULL之前沒有預取任何東西是絕對重要的。所以你需要一些東西來防止CPU像上面那樣重組代碼的內存操作。內存屏障做到這一點。

+0

如果我錯了,糾正我,但是if內的'pInstance'分配是否構成阻止這種過早讀取的順序點?即使在單線程的情況下,不排序也會使其中斷。 – ShadowRanger

+0

@ShadowRanger序列點是一個單線程的概念。它們完全不適用於跨線程同步,這涉及到內存操作發生的順序。你是否同意沒有內存障礙,我上面描述的優化是一個有效的預取? –

+1

不,這正是我的觀點。在'if'塊的內容可以重新分配'tmp'的任何情況下,獲取'tmp-> foo'都是無效的,因爲實際上,取消引用NULL將是無效的。即使在100%單線程情況下,編譯器/處理器也會這樣做。 (注意:有些處理器具有推測預取的概念,不需要有效地址,GCC公開了諸如'__builtin_prefetch'之類的東西,但這是一種不同的情況,因爲當預取是無效內存或者如果預取是功能性的,你稍後再讀一個不同的地址)。 – ShadowRanger

1

你爲什麼還在談論2004年的論文? C++ 11保證靜態變量只被初始化一次。這裏是你的fullly-工作,正確率100%單(其中,當然,是在它自己的反模式):

static TheTon& TheTon::instance() { 
    static TheTon ton; 
    return ton; 
} 
+2

這是一個很好的提示,以便那些有類似問題的人知道潛在更好的設計替代方案。然而,我的問題來自普遍的好奇心。此外,由於各種原因,Pre-C++ 11編譯器在業內仍然非常流行,並且存在許多必須維護的遺留代碼。 – user1747505