【问题标题】:Is this implementation of Double checked lock pattern (DCLP) in C++11 is correct?C++11 中双重检查锁定模式 (DCLP) 的实现是否正确?
【发布时间】:2015-06-02 04:58:19
【问题描述】:

我正在阅读有关 DCLP(双重检查锁定模式)的信息,但我不确定我是否正确。在使用原子创建锁时(如DCLP fixed in 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。这是一个理论问题。

【问题讨论】:

  • 您在修改后的版本中返回布尔值而不是 Singleton*...
  • 您不会“获得栅栏”或“释放栅栏”。栅栏具有获取或释放语义。

标签: c++ multithreading c++11 double-checked-locking


【解决方案1】:

如果我在“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&lt;bool&gt;)+sizeof(Singleton*) 可能超过sizeof(atomic&lt;Singleton*&gt;)),并且通过将逻辑分成两个变量(一个布尔值和一个指针)你让你更容易出错,就像你做的那样。因此,与原始指针相比,这样做没有任何优势,指针本身也用作布尔值,因为您直接查看指针,而不是查看某些可能未正确设置的布尔值。

【讨论】:

  • 优秀的答案!我只是有一个问题,将Singleton* tmp = m_instance.load(std::memory_order_acquire); 替换为Singleton* tmp = m_instance.load(std::memory_order_consume); 会安全吗?对里面的memory_order_relaxed会有影响吗?
  • @Alejandro,在这种情况下,我相信它是等效的,并且安全的,但没有优势(这是一个开放的研究主题,编译器应该如何实现 memory_order_consume 的依赖排序,所以它可能会甚至使用当今的编译器生成相同的代码)。它不会对松弛负载产生影响,因为它的顺序是由互斥锁强制执行的。
  • 谢谢!是的,我的意思是存储真实的,对此感到抱歉!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多