以下 C++ 代码中实现的 DCL(双重检查锁定)是不是是线程安全的?

Posted

技术标签:

【中文标题】以下 C++ 代码中实现的 DCL(双重检查锁定)是不是是线程安全的?【英文标题】:Is the DCL(double-checked locking) implemented in the following C ++ code thread-safe?以下 C++ 代码中实现的 DCL(双重检查锁定)是否是线程安全的? 【发布时间】:2020-05-08 12:57:06 【问题描述】:

这是一段 DCL(双重检查锁定)代码,由 C++ 中的“获取-释放”语义实现。代码如下:

std :: atomic <Singleton *> Singleton :: m_instance;
std :: mutex Singleton :: m_mutex;

Singleton * Singleton :: getInstance () 
    Singleton * tmp = m_instance.load (std :: memory_order_acquire); // 3
    if (tmp == nullptr) 
        std :: lock_guard <std :: mutex> lock (m_mutex);
        tmp = m_instance.load (std :: memory_order_relaxed);
        if (tmp == nullptr) 
            tmp = new Singleton; // 1
            m_instance.store (tmp, std :: memory_order_release); // 2
        
    
    return tmp;

在https://en.cppreference.com/w/cpp/atomic/memory_order上,memory_order_release的解释是: 具有此内存顺序的存储操作执行释放操作:在此存储之后,当前线程中的任何读取或写入都不能重新排序。当前线程中的所有写入在获取相同原子变量的其他线程中都是可见的。

我的理解是:load-store,store-store不能重排,但没说other-store不能重排。

所以我认为:'1'不仅包括读写指令,还包括调用指令,那么调用指令可能在'2'后面重新排序;那么 '3' 可能会得到一个不安全的 'tmp' 指针。

让我再描述一下上面的段落:

Disassemble ‘1’ into the following two possible pseudo-instructions:
tmp = allocate ();
call Singleton constructor (tmp); // 4

我认为“4”可能会在“2”之后重新排序。一个线程执行‘2’后,另一个线程完成‘3’并获得tmp指针。此时tmp指针是一个不安全的Singleton指针。

所以我有一个问题:上面的代码是线程安全的吗?

【问题讨论】:

FWIW,你可以通过Singleton&amp; Singleton :: getInstance () static Singleton instance; return instance; 获得所有这些内容 @NathanOliver 没错,c++11 保证了静态局部变量的线程安全。但我想知道这段代码是否线程安全? 【参考方案1】:

是的,很安全!

如果获取加载返回 null(即,单例尚未初始化),则您获取互斥锁。在互斥体内部,重新加载可以放松,因为m_instance 的修改无论如何都受互斥体保护,即,如果其他线程已经初始化了单例,那么该线程的互斥体释放必须在我们的互斥体获取之前发生操作,所以保证我们看到更新的m_instance

如果acquire-load(1)“看到”了release-store(2)写入的值,这两个操作会相互同步,从而建立happens-before关系,所以你可以安全地访问对象tmp指向。

更新 release-store 也受到互斥锁的保护,并且 不可能 tmp 的初始化部分与 store 重新排序。一般来说,应该避免争论可能的重新排序。该标准没有说明是否/如何重新排序操作。相反,它定义了(线程间)-happens-before 关系。编译器可能执行的任何重新排序仅仅是应用发生前关系规则的结果。

如果acquire-load(1)加载release-store(2)写入的值,这两个操作会同步,从而建立happens-before关系,即(2)happens-before(3 )。但是由于 (1) 是先于 (2) 排序的,并且发生之前的关系是传递的,因此必须保证 (1) 发生在 (3) 之前。因此,不可能用 (2) 重新排序 (1)(或其中的一部分)。

【讨论】:

“那个线程的互斥释放一定是在我们的互斥获取操作之前发生的”,没错。但是调用指令可能在'2'后面重新排序,所以,我们得到一个不安全的来自“3”的“tmp”指针。 对不起,我不太明白你的意思? release-store (2) 也受到互斥锁的保护。 我的表达不太好,我重新编辑了问题,你可以再看一遍。 我已经用更多细节更新了我的答案 - 我现在很清楚为什么你的代码是安全的。 非常感谢,我们应该使用 Happens-before 规则编程。

以上是关于以下 C++ 代码中实现的 DCL(双重检查锁定)是不是是线程安全的?的主要内容,如果未能解决你的问题,请参考以下文章

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

为啥在双重检查锁定中使用 Volatile.Write?

python Python中实现的单例,双重判断

糟糕的双重检查加锁(DCL)

双重检查锁定模式 - 在传递给 call_once 的 lambda 中捕获

DCL的单例一定是线程安全的吗