C++ 在双重检查锁定模式中获取屏障

C++ 在双重检查锁定模式中获取屏障,c++,multithreading,C++,Multithreading,在本文中,作者给出了一个如何正确实现该模式的示例 Singleton* Singleton::instance () { Singleton* tmp = pInstance; ... // insert memory barrier (1) if (tmp == 0) { Lock lock; tmp = pInstance; if (tmp == 0) { tmp = new Singleton; ..

在本文中,作者给出了一个如何正确实现该模式的示例

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;
}

这是必要的。否则,在
if
之后发生的读取可能会在复制之前由CPU预取,这将是一场灾难。如果
pInstance
不为空且我们没有获取任何锁,则必须保证在读取
pInstance
后发生的读取不会在读取
pInstance
之前重新排序

考虑:

Singleton* tmp = pInstance;
if (tmp == 0) { ... }
return tmp->foo;
如果CPU在
tmp
之前读取
tmp->foo
,会发生什么情况?例如,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
视为非空之前,不要预取任何内容。因此,您需要一些东西来防止CPU像上面那样重新组织代码的内存操作。记忆障碍可以做到这一点。

你为什么还在谈论2004年的论文?C++ 11保证静态变量只初始化一次。这是您的完整工作,100%正确的单例(当然,这本身就是一种反模式):


我不擅长猜测编译器可能会做什么,但即使它对正确性并不重要,维护
tmp
确实可以避免在常见(已初始化)情况下多次重新读取全局状态;如果您的周期非常困难,双重检查锁定似乎是一个好主意,避免重复读取全局状态可以确保您不会以其他方式牺牲一些收益
tmp
是堆栈本地的(因此不需要考虑共享);编译器可以安全地避免重读它,但可能无法优化第二次直接读取
pInstance
@ShadowRanger,因此我的修改可能是有效的,但作者选择了以这种方式实现您提到的优化?不幸的是,本文没有解释最终设计的原因,只是需要设置障碍。@user1747505您的更改不能保证将
pInstance
视为非空的线程可以看到基础对象的初始化。考虑到将
pInstance
视为非NULL的线程永远不会通过任何内存障碍,您认为哪种内存障碍可以确保它的安全。@DavidSchwartz很好的解释,谢谢。因此,考虑到您所说的(如果我在这里错了,您可以纠正我),将内存屏障1向下移动到刚好高于最终返回值似乎很好,但不能将其上移到函数的第一行。如果屏障正好在返回语句之前,我的第二个代码示例将是有效的。如果我错了,请纠正我,但是
if
pInstance
的赋值不会构成防止这种过早读取的序列点吗?在单线程的情况下,不进行排序会使这个问题变得更糟。@ShadowRanger序列点是一个单线程的概念。它们完全不适用于涉及内存操作发生顺序的线程间同步。你同意没有内存障碍,我上面描述的优化是有效的预取吗?不,这正是我的观点。在
if
块的内容可以重新分配
tmp
的任何情况下,获取
tmp->foo
都是无效的,因为实际取消引用
NULL
是无效的;这样做的编译器/处理器即使在100%单线程的情况下也会出现故障。(注意:一些处理器有一个推测性预取的概念,它不需要有效的地址,GCC用
\uuuuuu builtin\u prefetch
之类的东西公开了这一点,但这是另一种情况,因为当预取内存无效或以后读取不同的地址时,它在功能上是noop)@ShadowRanger:虽然我承认我不知道在真正的处理器上有什么方法可以在内存屏障1和使用
pInstance
pInstance->foo
的代码之间加载缓存线,但事实上整个初始化和内存屏障2以及发布都可以在第二个内核上进行,在第一个内核执行内存屏障#1之后。那么内存屏障#1将不会与
pInstance->foo
的更新交互。我猜,对另一个恰好共享缓存线的变量的推测性读取可能会用过时的数据填充缓存。非常好的信息。唯一会发生这种情况的真正CPU可能是DEC alpha(生命周期结束)。在今天的x86和ARM CPU上,内存屏障#2不仅确保屏障之前的存储在屏障之后的存储发送到总线,而且还确保其他CPU以相同的顺序“观察”这些存储,即如果pInstance指向有效地址,然后指向pInstance的内容也将是最新的内容。这是一个很好的观点,可以让有类似问题的人知道可能更好的设计替代方案。不过,我的问题来自普遍的好奇心。此外,由于各种原因,pre-C++11编译器仍然在业界大量使用,并且有许多遗留代码需要维护。
static TheTon& TheTon::instance() {
    static TheTon ton;
    return ton;
}