为双重检查锁定提供适当的编译器内在特性

Proper compiler intrinsics for double-checked locking?

本文关键字:编译器 检查 锁定      更新时间:2023-10-16

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

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

在弱顺序内存体系结构上,我的理解是,在写入MyClass::CreateMyClass::MyClass的其他变量可见之前,s_object的新值可能对其他线程可见。此外,编译器本身可以在没有编译器屏障的情况下以这种方式排列代码(在Visual c++中,_WriteBarrier,但_InterlockedCompareExchange充当屏障)。

我是否需要像一个存储栅栏内在函数在那里或一些东西,以确保MyClass的变量是可见的所有线程之前,s_object成为-1以外的东西?

幸运的是,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;

对于一个正确的c++ 11实现,任何函数局部static变量都将由通过该变量的第一个线程以线程安全的方式构造。