C ++ 11中的双重检查锁定模式(DCLP)的实现是否正确?

时间:2015-06-02 04:58:19

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

我正在阅读关于DCLP(双重检查锁定模式),我不确定我是否正确。当使用原子来创建锁时(如DCLP fixed in C++11中所述),有两件事情不明确:

  1. 在文章的代码中:
  2. \/

    如果我在“load()”中获取围栏会发生什么,但是tmp不是nullptr,我只是返回?难道我们不应该说出CPU可以“释放栅栏”的位置吗?

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

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

    1. 如果我正确地得到了这篇文章,那么这也是实现DCLP的正确方法吗?
    2. 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;
      }
      

      换句话说,不是查看实例,而是使用原子布尔值来确保DCLP正常工作,而第二个tmp内部的任何内容都可以同步并运行一次。这是对的吗?

      谢谢!

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

1 个答案:

答案 0 :(得分:3)

  

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

没有。当商店进入m_instance时,就会完成发布。如果你加载m_instance并且它不是null,那么发布已经发生在早期,你不需要这样做。

你没有“获得围栏”和“释放围栏”,就像你获得了一个互斥锁一样。这不是什么围栏。围栏只是一个没有相关内存位置的获取或释放操作。并且围栏在这里并不真正相关,因为所有获取和释放操作都有一个相关的内存位置(原子对象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*>)),并将逻辑拆分为两个变量(布尔值和一个指针)就像你做的那样,你更容易出错。因此,与原始方法相比没有优势,其中指针本身也用作布尔值,因为您直接查看指针,而不是某些可能未正确设置的布尔值。