在实现双重检查锁定时,在实现初始化双重检查锁定时,执行内存和/或编译器屏障的正确方法是什么?
像 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
的新值可能是在 MyClass::Create
内写入的其他变量之前对其他线程可见或MyClass::MyClass
是可见的。此外,编译器本身可以在没有编译器屏障的情况下以这种方式安排代码(在 Visual C++ 中为 _WriteBarrier
,但 _InterlockedCompareExchange
充当屏障)。
我是否需要像商店栅栏那样的内在功能或其他东西来确保 MyClass
的变量对 s_object
之前的所有线程都可见变得除了-1
之外的东西?
最佳答案
幸运的是,C++ 中的规则非常简单:
If there is a data race, the behaviour is undefined.
在您的代码中,数据争用是由以下读取引起的,它与 __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;
关于c++ - 用于双重检查锁定的正确编译器内在函数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/24104140/