2015-10-14 68 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