双重检查锁定的正确编译器内在函数?

Posted

技术标签:

【中文标题】双重检查锁定的正确编译器内在函数?【英文标题】:Proper compiler intrinsics for double-checked locking? 【发布时间】:2014-06-08 07:59:41 【问题描述】:

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

像 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::CreateMyClass::MyClass 中的其他变量是可见的。此外,编译器本身可以在没有编译器屏障的情况下以这种方式排列代码(在 Visual C++ 中,_WriteBarrier,但 _InterlockedCompareExchange 充当屏障)。

s_object 变成除-1 之外的其他东西之前,我是否需要像其中的存储栅栏内在函数或其他东西以确保MyClass 的变量对所有线程可见?

【问题讨论】:

C++11: static global = initialize_global() 是线程安全的。 另请注意:“双重检查锁定”模式是一种损坏模式。见 (en.wikipedia.org/wiki/Double-checked_locking) 1.请参阅我对 Dietmar 的回复,了解为什么函数静力学不起作用。 2.由于内存语义薄弱,双重检查锁定被破坏。这是关于使用编译器内在函数进行正确的内存同步,以免中断。换句话说,我很清楚这一点。 你可以求助于 std::call_once 或 boost::call_once @Dieter 不,不是。该模式只有一些损坏的实现,但该模式本身是有效的。 【参考方案1】:

幸运的是,C++ 中的规则非常简单:

如果存在数据竞争,则行为未定义。

在您的代码中,数据竞争是由以下读取引起的,这与__InterlockedCompareExchangePointer 中的写入操作冲突。

if (s_object.m_void == UNSET_POINTER)

没有阻塞的线程安全解决方案可能如下所示。请注意,在 x86 上,具有顺序一致性的加载操作与常规加载操作相比基本上没有开销。如果你关心其他架构,你也可以使用获取发布而不是顺序一致性

static std::atomic<MyClass*> s_objectnullptr;

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 是因为空指针在我的情况下是一个有效的“初始化”状态,所以我需要一种方法来区分空指针和未初始化。除了缺少 nullptr/-1 区别之外,我不确定您的实现与我的实现有何不同;我查看了这个 std::atomic::compare_exchange_strong 在 Visual C++ 中的实际作用,它只是 _InterlockedCompareExchange。发生在 MyClass::MyClass 中的写入呢?它们是否可以在弱排序架构上的比较交换之后重新排序,从而破坏? @Myria:你是对的。从代码中不清楚。 s_objects_object.load(std::memory_order_seq_cst) 相同。这会有所不同。 nullptr 只是为了简化示例。 对此我深表歉意。联合跛行是由于我构建示例的方式 - 它最初将使用函数指针。由于 _InterlockedCompareExchange 采用 void * 而不是 void (*)() 之类的东西,因此需要联合来比较交换函数指针。我已经编辑了我的原始代码以删除联合。您的新 compare_exchange_strong 第三个参数会导致内存围栏?【参考方案2】:

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

【讨论】:

微软还没有实现这个; Visual Studio 2013 不支持 C++11 的这个特性,所以它不能在 Windows 上运行(因为我的程序也在 Windows 构建中使用了异常处理,clang/GCC 不支持)。他们终于在 Visual Studio 2015 中实现它,该版本于周二发布了社区演示版;自然要到 2015 年才会发布。

以上是关于双重检查锁定的正确编译器内在函数?的主要内容,如果未能解决你的问题,请参考以下文章

SSE 内在函数检查零标志

双重检查锁定原理详解

JAVA 双重检查锁定和延迟初始化

双重检查和锁定模式是不是适用于 C++(不是 11)?

java双重检查锁定

双重检查锁定与延迟初始化