12

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

我只是想知道第一个内存屏障是否可以移动到 return 语句的正上方?

编辑:另一个问题:在链接的文章中,正如vidstige 所引用的

从技术上讲,您不需要完全的双向屏障。第一个障碍必须防止 Singleton 的构造向下迁移(通过另一个线程);第二个屏障必须防止 pInstance 的初始化向上迁移。这些被称为“获取”和“释放”操作,并且可能会产生比硬件(例如Itainum)上的完全障碍更好的性能,从而做出区分。

它说第二个屏障不需要是双向的,那么它如何防止对 pInstance 的分配在该屏障之前移动?尽管第一个屏障可以阻止向上迁移,但另一个线程仍然有机会看到未初始化的成员。

编辑我想我几乎理解第一个障碍的目的。正如sonicoder 所指出的,当 if 返回 true 时,分支预测可能会导致 tmp 为 NULL。为了避免这个问题,必须有一个获取屏障来防止在读取 if 之前读取 tmp 作为返回值。

第一个屏障与第二个屏障配对以实现同步关系,因此可以向下移动。

编辑:对于那些对此问题感兴趣的人,我强烈建议阅读memory-barriers.txt

4

2 回答 2

5

我在这里没有看到与您的问题相关的任何正确答案,所以我决定即使在三年多之后发布一个;)

我只是想知道第一个内存屏障是否可以移动到 return 语句的正上方?

是的,它可以。

它用于不会进入if语句的线程,即pInstance已经正确构造和初始化,并且是可见的。

第二个屏障(前一个pInstance = tmp;)保证单例成员字段的初始化在提交之前提交到内存pInstance = tmp;但这并不一定意味着其他线程(在其他内核上)将以相同的顺序看到这些内存效应(违反直觉,对吗?)。第二个线程可能会看到缓存中指针的新值,但还没有看到那些成员字段。当它通过取消引用指针(例如,p->data)来访问成员时,该成员的地址可能已经在缓存中,但不是所需的地址。砰! 读取了错误的数据。请注意,这不仅仅是理论上的。有些系统需要执行高速缓存一致性指令(例如,内存屏障)以从内存中提取新数据。

这就是为什么第一个障碍在那里。它还解释了为什么可以将它放在return语句之前(但它必须放在之后Singleton* tmp = pInstance;)。

它说第二个屏障不需要是双向的,那么它如何防止对 pInstance 的分配在该屏障之前移动?

写屏障保证在它之前的每一个写操作都会在它之后的每一个写操作之前有效地发生。这是一个停车标志,没有文字可以越过它到另一边。有关更详细的说明,请参阅此处

于 2014-08-19T05:46:46.063 回答
4

不,内存屏障不能移动到赋值语句下方,因为内存屏障保护赋值不向上迁移。从链接的文章:

第一个障碍必须防止 Singleton 的构造向下迁移(通过另一个线程);第二个屏障必须防止 pInstance 的初始化向上迁移。

附带说明:单例的双重检查锁定模式仅在您有巨大的性能要求时才有用。

您是否分析过您的二进制文件并将单例访问视为瓶颈?如果不是,您根本不需要使用双重检查锁定模式。

我建议使用简单的锁。

于 2011-02-19T13:52:59.440 回答