双重检查锁定模式
在 C++ 和双重检查锁定的危险,有 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 引用
从技术上讲,您不需要完全的双向屏障。第一个障碍 必须防止 Singleton 构造向下迁移 (通过另一个线程);第二个障碍必须防止向上迁移 pInstance 的初始化。这些被称为“获取”并且 “释放”操作,可能会产生比完整操作更好的性能 硬件(例如 Itanum)上的障碍造成了区别。
它说第二个屏障不需要是双向的,那么它如何防止对 pInstance 的分配移动到该屏障之前呢?尽管第一个屏障可以阻止向上迁移,但另一个线程仍然有机会看到未初始化的成员。
编辑:我想我几乎明白了第一个障碍的目的。正如 sonicoder 所指出的,当 if 返回 true 时,分支预测可能会导致 tmp 为 NULL。为了避免这个问题,必须有一个获取屏障来防止在读入 if 之前返回读取 tmp。
第一个障碍物与第二个障碍物配对以实现同步关系,因此可以向下移动。
编辑:对于那些对此问题感兴趣的人,我强烈建议阅读内存屏障.txt。
In C++ and the Perils of Double-Checked Locking, there's persudo code to implement the pattern correctly which is suggested by the authors. See below,
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;
}
I just wonder that whether the first memory barrier can be moved right above the return statement?
EDIT: Another question: In the linked article, as vidstige quoted
Technically, you don’t need full bidirectional barriers. The first barrier
must prevent downwards migration of Singleton’s construction
(by another thread); the second barrier must prevent upwards migration
of pInstance’s initialization. These are called ”acquire” and
”release” operations, and may yield better performance than full
barriers on hardware (such as Itainum) that makes the distinction.
It says that the second barrier doesn't need to be bidirectional, so how can it prevent the assignment to pInstance from being moved before that barrier? Even though the first barrier can prevent upwards migration, but another thread can still have chance to see the un-initialized members.
EDIT: I think I almost understand the purpose of the first barrier. As sonicoder noted, branch prediction may cause tmp to be NULL when the if returns true. To avoid that problem, there must be a acquire barrier to prevent the reading of tmp in return before the reading in if.
The first barrier is paired with the second barrier to achieve synchronize-with relationship, so it can be move down.
EDIT: For those who are interested in this question, I strongly recommend reading memory-barriers.txt.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我在这里没有看到任何与你的问题相关的正确答案,所以我决定在三年多后发布一个;)
是的,可以。
适用于不会进入
if
语句的线程,即pInstance
已经被正确构造和初始化,并且是可见的。第二个屏障(
pInstance = tmp;
之前的那个)保证在提交pInstance = tmp;
之前将单例成员字段的初始化提交到内存。 但这并不一定意味着其他线程(在其他内核上)会以相同的顺序看到这些内存效应(违反直觉,对吧?)。第二个线程可能会看到缓存中指针的新值,但还看不到那些成员字段。当它通过取消引用指针来访问成员时(例如,p->data
),该成员的地址可能已经在缓存中,但不是所需的地址。砰!读取了错误的数据。请注意,这不仅仅是理论上的。 有些系统您需要执行缓存一致性指令(例如,内存屏障)从内存中提取新数据。这就是为什么第一个障碍存在。它还解释了为什么可以将其放在
return
语句之前(但必须放在Singleton* tmp = pInstance;
之后)。写屏障保证它前面的每个写操作都会在它后面的每个写操作之前有效发生。这是一个停止标志,任何写入都不能越过它到达另一边。有关更详细的说明,请参阅此处。
I didn't see any correct answer here related to your question so I decide to post one even after more than three years;)
Yes, it can.
It's for threads that won't enter the
if
statement, i.e.,pInstance
has already been constructed and initialized correctly, and is visible.The second barrier (the one right before
pInstance = tmp;
) guarantees that the initialization of singleton's members fields are committed to memory beforepInstance = tmp;
is committed. But this does NOT necessarily mean that other threads (on other cores) will see these memory effects in the same order (counter-intuitive, right?). A second thread may see the new value of the pointer in cache but not those member fields yet. When it accesses a member by dereferencing the pointer (e.g.,p->data
), the address of that member may has already been in cache, but not the one that's desired. Bang! A wrong data is read. Note that this is more than theoretical. There are systems that you need perform a cache coherence instruction (e.g., a memory barrier) to pull new data from memory.That's why the first barrier is there. It also explains why it's ok to place it right before the
return
statement (but it has to be afterSingleton* tmp = pInstance;
).A write barrier guarantees that every write preceding it will effectively happen before every write following it. It's a stop sign, and no write can cross it to the other side. For a more detailed description, refer to here.
不可以,内存屏障不能移动到赋值语句下方,因为内存屏障保护赋值不向上迁移。来自链接的文章:
附带说明:单例的双重检查锁定模式仅在您有巨大的性能要求时才有用。
您是否分析过您的二进制文件并观察到单例访问是一个瓶颈?如果没有,您可能根本不需要担心双重检查锁定模式。
我建议使用简单的锁。
No, the memory barrier cannot be moved below the assignment-statement since the memory barrier protects the assignment from upwards migration. From the linked article:
On a side note: The double-checked locking pattens for singletons is only useful if you have huge performance requirements.
Have you profiled your binaries and observed the singleton access as a bottle-neck? If not chances are you do not need to bother at all with the double-checked locking pattern.
I recommend using a simple lock.