我正在阅读关于DCLP(双重检查锁定模式),我不确定我是否正确。当使用原子来创建锁时(如DCLP fixed in C++11中所述),有两件事情不明确:
\/
如果我在“load()”中获取围栏会发生什么,但是tmp不是nullptr,我只是返回?难道我们不应该说出CPU可以“释放栅栏”的位置吗?
如果不需要释放围栏,我们为什么要获得并释放?有什么区别?
Surly我错过了一些基本的东西......
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。这是一个理论问题。
答案 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*>)
),并将逻辑拆分为两个变量(布尔值和一个指针)就像你做的那样,你更容易出错。因此,与原始方法相比没有优势,其中指针本身也用作布尔值,因为您直接查看指针,而不是某些可能未正确设置的布尔值。