实现双重检查锁定时,在实现双重检查锁定以进行初始化时,执行内存和/或编译器障碍的正确方法是什么?
像std :: call_once这样的东西不是我想要的;这太慢了。它通常只是在操作系统的pthread_mutex_lock和EnterCriticalSection之上实现。
在我的程序中,我经常遇到初始化情况,初始化可以安全地重复,只要一个线程只能设置最终指针。如果另一个线程击败它以设置指向单例对象的最终指针,它将删除它创建的内容并使用其他线程。我也经常使用这种情况,因为它们都会得到相同的结果,因为哪个线程“赢”并不重要。
这是一个使用Visual C ++内在函数的不安全,过于人为的例子:
MyClass *GetGlobalMyClass()
{
static MyClass *const UNSET_POINTER = reinterpret_cast<MyClass *>(
static_cast<intptr_t>(-1));
static MyClass *volatile s_object = UNSET_POINTER;
if (s_object == UNSET_POINTER)
{
MyClass *newObject = MyClass::Create();
if (_InterlockedCompareExchangePointer(&s_object, newObject,
UNSET_POINTER) != UNSET_POINTER)
{
// Another thread beat us. If Create didn't return null, destroy.
if (newObject)
{
newObject->Destroy(); // calls "delete this;", presumably
}
}
}
return s_object;
}
在弱有序的内存架构上,我的理解是s_object
的新值可能在在<{1}}内写入的其他变量之前对其他线程可见。 MyClass::Create
可见。此外,编译器本身可以在没有编译器障碍的情况下以这种方式排列代码(在Visual C ++中,MyClass::MyClass
,但_WriteBarrier
充当障碍)。
我是否需要像那里的商店围栏内在函数一样,以确保_InterlockedCompareExchange
的变量在MyClass
成为s_object
以外的某些东西之前对所有线程都可见?< / p>
答案 0 :(得分:2)
幸运的是,C ++中的规则非常简单:
如果存在数据争用,则行为未定义。
在您的代码中,数据争用是由以下读取引起的,这与__InterlockedCompareExchangePointer
中的写入操作冲突。
if (s_object.m_void == UNSET_POINTER)
没有阻塞的线程安全解决方案可能如下所示。请注意,在x86上,与常规加载操作相比,具有顺序一致性的加载操作基本上没有开销。如果您关心其他架构,也可以使用获取版本而不是顺序一致性。
static std::atomic<MyClass*> s_object{nullptr};
MyClass* o = s_object.load(std::memory_order_seq_cst);
if (o == nullptr) {
o = new MyClass{...};
MyClass* expected = nullptr;
if (!s_object.compare_exchange_strong(expected, o, std::memory_order_seq_cst)) {
delete o;
o = expected;
}
}
return o;
答案 1 :(得分:0)
对于正确的C ++ 11实现,任何函数本地static
变量将由第一个通过此变量的线程以线程安全的方式构造。