在C++ and the Perils of Double-Checked Locking中,有一些persudo代码可以正确地实现模式,这是作者建议的。见下文,
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;
}
我只是想知道第一个内存屏障是否可以在return语句的正上方移动?
编辑:另一个问题:在链接文章中,引用vidstige
从技术上讲,您不需要完全双向障碍。第一道屏障 必须防止单身人士的建设向下迁移 (通过另一个线程);第二道屏障必须防止向上迁移 pInstance的初始化。这些被称为“获得”和 “释放”操作,可能会产生比完整更好的性能 硬件上的障碍(例如Itainum)作出区分。
它说第二个屏障不需要是双向的,那么如何防止pInstance的赋值在该屏障之前被移动?即使第一道屏障可以阻止向上迁移,但另一个线程仍然有机会看到未初始化的成员。
编辑:我想我几乎理解了第一道屏障的目的。正如sonicoder所指出的,当if返回true时,分支预测可能导致tmp为NULL。为了避免这个问题,必须有一个获取障碍,以防止在读取if之前读取tmp。 击>
第一个屏障与第二个屏障配对以实现与同步关系,因此可以向下移动。
编辑:对于对此问题感兴趣的人,我强烈建议您阅读memory-barriers.txt。
答案 0 :(得分:5)
我没有看到任何与您的问题相关的正确答案,所以我决定在三年多之后发布一个;)
我只是想知道第一个记忆障碍是否能够正确移动 在返回声明之上?
是的,它可以。
对于不会进入if
语句的线程,即pInstance
已经构建并正确初始化,并且可见。
第二个屏障(pInstance = tmp;
之前的屏障)保证在提交pInstance = tmp;
之前将单例成员字段的初始化提交到内存。 但这并不一定意味着其他线程(在其他核心上)会以相同的顺序看到这些记忆效应 (反直觉,对吧?)。第二个线程可能会在缓存中看到指针的新值,但尚未看到那些成员字段。当它通过解除引用指针(例如,p->data
)来访问成员时,该成员的地址可能已经在高速缓存中,但不是所需的地址。砰!读取错误的数据。请注意,这不仅仅是理论上的。 There are systems您需要执行缓存一致性指令(例如,内存屏障)以从内存中提取新数据。
这就是为什么第一道屏障就在那里。它还解释了为什么可以在return
语句之前放置它(但它必须在Singleton* tmp = pInstance;
之后。)
它说第二道屏障不需要是双向的,所以 它如何防止pInstance的赋值被移动之前 那障碍?
写屏障保证 在它之后的每次写入之前的每次写入都会有效地发生 。这是一个停止标志,没有写入可以将它交叉到另一边。有关更详细的说明,请参阅here。
答案 1 :(得分:4)
不,内存屏障不能移动到赋值语句之下,因为内存屏障可以保护分配不被向上迁移。来自链接的文章:
第一道屏障必须防止 单身人士的向下迁移 建筑(由另一个线程);该 第二道屏障必须防止向上 pInstance的迁移 初始化。
侧面说明:单一的双重检查锁定模式仅在您有巨大的性能要求时才有用。
你有没有想过你的二进制文件并将单身人士访问视为瓶颈?如果没有机会你不需要打扰双重锁定模式。
我建议使用简单的锁。