1

我正在阅读有关 DCLP(双重检查锁定模式)的信息,但我不确定我是否正确。当使用原子来创建锁时(如DCLP 中所述,在 C++11中修复),有两件事不清楚:

  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 可以在哪里“释放围栏”吗?

如果不需要释放栅栏,那我们为什么要获取和释放?有什么区别?

Surly我错过了一些基本的东西......

  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 内的任何内容都必须同步并运行一次。这是对的吗?

谢谢!

编辑:请注意,我不是在问实现单例的问题,而只是为了更好地理解栅栏和原子的概念以及它如何修复 DCLP。这是一个理论问题。

4

1 回答 1

4

如果我在“load()”中获取栅栏,但 tmp 不是 nullptr,我只是返回,会发生什么情况?我们不应该说明 CPU 可以在哪里“释放围栏”吗?

不,当存储m_instance发生时,释放就完成了。如果您加载m_instance并且它不为空,那么该版本已经在较早的时候发生了,您不需要这样做。

您不会像获得互斥锁那样“获得栅栏”和“释放栅栏”。这不是栅栏。栅栏只是一个没有关联内存位置的获取或释放操作。栅栏在这里并不重要,因为所有的获取和释放操作都有一个关联的内存位置(原子对象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,是一个获取操作。内存模型说,非空指针的存储与任何看到非空指针的加载同步,这意味着new Singleton在任何线程可以从tmp. 这修复了 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*>)),并且通过将逻辑分成两个变量(一个布尔值和一个指针)你更容易出错,因为你做过。因此,与原始指针相比,这样做没有任何优势,指针本身也用作布尔值,因为您直接查看指针,而不是某些可能未正确设置的布尔值。

于 2015-06-02T10:13:46.530 回答