C++11 中双重检查锁定模式 (DCLP) 的实现是否正确?
Is this implementation of Double checked lock pattern (DCLP) in C++11 is correct?
我正在阅读有关 DCLP(双重检查锁定模式)的信息,但我不确定自己是否理解正确。使用原子创建锁时(如DCLP fixed in C++11中所述),有两点不清楚:
- 文章中的代码:
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,而我只是 return,会发生什么情况?我们不应该说明 CPU 在哪里可以 "release the fence" 吗?
如果不需要释放fence,那为什么要有acquire和release呢?有什么区别?
抱歉,我缺少一些基本的东西....
- 如果我没看错那篇文章,这也是实现 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。这是一道理论题。
What happens if I aquire the fence inside "load()", but than tmp is not nullptr, and I simply return? Shouldn't we state where the CPU can "release the fence"?
没有。当存储到 m_instance
时,发布完成。如果你加载 m_instance
并且它不为空,那么发布已经发生了,你不需要这样做。
您不像获取互斥锁那样"acquire a fence"和"release a fence"。栅栏不是这样的。栅栏只是一个没有关联内存位置的获取或释放操作。栅栏在这里并不真正相关,因为所有获取和释放操作都有一个关联的内存位置(原子对象 m_instance
)。
您不必像互斥锁+解锁那样成对地获取+释放。您可以执行一个释放操作来存储一个值,并进行任意数量的获取操作(零个或多个)来加载该值并观察其效果。
loads/stores 上的 acquire/release 语义与 load/store 任一侧的操作顺序相关,以防止重新排序。
对变量 A 的非松散原子存储(即释放操作)将与稍后的相同的非松散原子加载(即获取操作)同步变量 A.
正如 C++ 标准所说:
Informally, performing a release operation on A forces prior side
effects on other memory locations to become visible to other threads that later perform a consume or an acquire operation on 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
的存储在对象完全构造之前可能对其他线程可见。
In other words, instead of looking at the instance I am using an atomic boolean to make sure the DCLP works, and whatever is inside the second tmp is surly to be syncronized and run once. Is it correct?
否,因为您将 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*>)
多),并且通过将逻辑分成两个变量(a布尔值和一个指针),就像您所做的那样,您更容易出错。因此,与原始方法相比,这样做没有任何优势,指针本身也用作布尔值,因为您直接查看指针,而不是某些可能未正确设置的布尔值。
我正在阅读有关 DCLP(双重检查锁定模式)的信息,但我不确定自己是否理解正确。使用原子创建锁时(如DCLP fixed in C++11中所述),有两点不清楚:
- 文章中的代码:
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,而我只是 return,会发生什么情况?我们不应该说明 CPU 在哪里可以 "release the fence" 吗?
如果不需要释放fence,那为什么要有acquire和release呢?有什么区别?
抱歉,我缺少一些基本的东西....
- 如果我没看错那篇文章,这也是实现 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。这是一道理论题。
What happens if I aquire the fence inside "load()", but than tmp is not nullptr, and I simply return? Shouldn't we state where the CPU can "release the fence"?
没有。当存储到 m_instance
时,发布完成。如果你加载 m_instance
并且它不为空,那么发布已经发生了,你不需要这样做。
您不像获取互斥锁那样"acquire a fence"和"release a fence"。栅栏不是这样的。栅栏只是一个没有关联内存位置的获取或释放操作。栅栏在这里并不真正相关,因为所有获取和释放操作都有一个关联的内存位置(原子对象 m_instance
)。
您不必像互斥锁+解锁那样成对地获取+释放。您可以执行一个释放操作来存储一个值,并进行任意数量的获取操作(零个或多个)来加载该值并观察其效果。
loads/stores 上的 acquire/release 语义与 load/store 任一侧的操作顺序相关,以防止重新排序。
对变量 A 的非松散原子存储(即释放操作)将与稍后的相同的非松散原子加载(即获取操作)同步变量 A.
正如 C++ 标准所说:
Informally, performing a release operation on A forces prior side effects on other memory locations to become visible to other threads that later perform a consume or an acquire operation on 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
的存储在对象完全构造之前可能对其他线程可见。
In other words, instead of looking at the instance I am using an atomic boolean to make sure the DCLP works, and whatever is inside the second tmp is surly to be syncronized and run once. Is it correct?
否,因为您将 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*>)
多),并且通过将逻辑分成两个变量(a布尔值和一个指针),就像您所做的那样,您更容易出错。因此,与原始方法相比,这样做没有任何优势,指针本身也用作布尔值,因为您直接查看指针,而不是某些可能未正确设置的布尔值。