这种在C++11中实现双重检查锁模式(DCLP)是否正确

Is this implementation of Double checked lock pattern (DCLP) in C++11 is correct?

本文关键字:DCLP 模式 是否 检查 C++11 实现      更新时间:2023-10-16

我正在读关于DCLP(双重检查锁模式(的文章,但我不确定我是否做对了。当使用原子创建锁时(如C++11中修复的DCLP中所解释的(,有两件事不清楚:

  1. 在文章的代码中:
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_acquire);
    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;
            m_instance.store(tmp, std::memory_order_release);
        }
    }
    return tmp;
}

如果我在"load(("中获取了围栏,但tmp不是nullptr,并且我只是返回,会发生什么?难道我们不应该说明CPU可以在哪里"释放围栏"吗?

如果不需要释放围栏,那为什么我们必须获得并释放?有什么区别?

我确实错过了一些基本的东西。。。。

  1. 如果我正确地理解了这篇文章,那么这也是实现DCLP的正确方法吗
Singleton* Singleton::m_instance = null;
std::atomic<bool> Singleton::is_first; // init to false
std::mutex Singleton::m_mutex;
Singleton* Singleton::getInstance() {
    bool tmp = is_first.load(std::memory_order_acquire);
    if (tmp == false) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = is_first.load(std::memory_order_relaxed);
        if (tmp == false) {
            // can place any code that will run exactly once!
            m_instance = new Singleton;
            // store back the tmp atomically
            is_first.store(tmp, std::memory_order_release);
        }
    }
    return m_instance;
}

换句话说,我没有查看实例,而是使用原子布尔值来确保DCLP工作,并且第二个tmp中的任何内容都必须同步并运行一次。这是正确的吗?

谢谢!

EDIT:注意,我不是在问实现singleton的问题,只是为了更好地理解fence和atomic的概念,以及它是如何修复DCLP的。这是一个理论问题。

如果我在"load(("中获取了围栏,但tmp不是nullptr,并且我只是返回,会发生什么?难道我们不应该说明CPU可以在哪里"释放围栏"吗?

没有。当存储到m_instance时,就完成了释放。如果你加载了m_instance并且它不是null,那么发布已经提前了,你不需要这么做

您不会像获取互斥锁那样"获取围栏"answers"释放围栏"。围栏不是这样的。围栏只是一种获取或释放操作,没有相关的内存位置。围栏在这里并不真正相关,因为所有的获取和释放操作都有一个相关的内存位置(原子对象m_instance(。

您不必像互斥锁+解锁那样在匹配的对中获得+释放。您可以执行一个释放操作来存储值,并进行任意数量的获取操作(零个或多个(来加载该值并观察其效果。

加载/存储的获取/释放语义与加载/存储两侧的操作顺序有关,以防止重新排序。

变量A的非松弛原子存储(即释放操作(将与相同变量A的后续非松弛原子加载(即获取操作(同步。

正如C++标准所说:

非正式地,在a上执行释放操作迫使前一方对其他内存位置的影响,使其对稍后在a.上执行消耗或获取操作的其他线程可见

因此,在您引用的DCLP代码中,m_instance.store(tmp, memory_order_release)m_instance的存储,是一个发布操作。m_instance.load(memory_order_acquire)是来自m_instance的负载,并且是获取操作。内存模型表示,非空指针的存储与看到非空指针时的任何加载同步,这意味着在任何线程可以从tmp加载非空值之前,可以保证new Singleton的所有效果都已完成。这修复了C++11之前的双重检查锁定的问题,其中tmp的存储可能在对象完全构造之前对其他线程可见。

换句话说,我没有查看实例,而是使用原子布尔值来确保DCLP工作,并且第二个tmp中的任何内容都必须同步并运行一次。这是正确的吗?

不,因为您将false存储在此处:

        // store back the tmp atomically
        is_first.store(tmp, std::memory_order_release);

这意味着在对该函数的下一次调用中,您将创建另一个Singleton并泄漏第一个。应该是:

        is_first.store(true, std::memory_order_release);

如果你解决了这个问题,我认为这是正确的,但在典型的实现中,它使用了更多的内存(sizeof(atomic<bool>)+sizeof(Singleton*)可能比sizeof(atomic<Singleton*>)多(,并且通过将逻辑拆分为两个变量(一个布尔值和一个指针(,你会更容易出错,就像你所做的那样。因此,与原始指针相比,这样做没有任何好处,因为指针本身也充当布尔值,因为你可以直接查看指针,而不是某些可能设置不正确的布尔值。