适当的编译器内在函数用于双重检查锁定?

时间:2014-06-08 07:59:41

标签: c++ multithreading locking intrinsics

实现双重检查锁定时,在实现双重检查锁定以进行初始化时,执行内存和/或编译器障碍的正确方法是什么?

像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>

2 个答案:

答案 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变量将由第一个通过此变量的线程以线程安全的方式构造。