C++ 上的双重检查锁定:新建临时指针,然后将其分配给实例
下面的单例实现有什么问题吗?
Foo& Instance() {
if (foo) {
return *foo;
}
else {
scoped_lock lock(mutex);
if (foo) {
return *foo;
}
else {
// Don't do foo = new Foo;
// because that line *may* be a 2-step
// process comprising (not necessarily in order)
// 1) allocating memory, and
// 2) actually constructing foo at that mem location.
// If 1) happens before 2) and another thread
// checks the foo pointer just before 2) happens, that
// thread will see that foo is non-null, and may assume
// that it is already pointing to a a valid object.
//
// So, to fix the above problem, what about doing the following?
Foo* p = new Foo;
foo = p; // Assuming no compiler optimisation, can pointer
// assignment be safely assumed to be atomic?
// If so, on compilers that you know of, are there ways to
// suppress optimisation for this line so that the compiler
// doesn't optimise it back to foo = new Foo;?
}
}
return *foo;
}
Anything wrong with the following Singleton implementation?
Foo& Instance() {
if (foo) {
return *foo;
}
else {
scoped_lock lock(mutex);
if (foo) {
return *foo;
}
else {
// Don't do foo = new Foo;
// because that line *may* be a 2-step
// process comprising (not necessarily in order)
// 1) allocating memory, and
// 2) actually constructing foo at that mem location.
// If 1) happens before 2) and another thread
// checks the foo pointer just before 2) happens, that
// thread will see that foo is non-null, and may assume
// that it is already pointing to a a valid object.
//
// So, to fix the above problem, what about doing the following?
Foo* p = new Foo;
foo = p; // Assuming no compiler optimisation, can pointer
// assignment be safely assumed to be atomic?
// If so, on compilers that you know of, are there ways to
// suppress optimisation for this line so that the compiler
// doesn't optimise it back to foo = new Foo;?
}
}
return *foo;
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(7)
不,您甚至不能假设 foo = p; 是原子的。它可能会加载 32 位指针的 16 位,然后在加载其余部分之前将其交换出去。
如果此时另一个线程潜入并调用
Instance()
,您就会被干掉,因为您的foo
指针无效。为了真正的安全性,您必须保护整个测试和设置机制,即使这意味着即使在构建指针之后也要使用互斥体。换句话说(我假设
scoped_lock()
当超出范围时会释放锁(我对 Boost 没什么经验)),比如:如果你不想互斥锁(可能是出于性能原因),我过去使用的一个选项是在线程开始之前构建所有单例。
换句话说,假设您拥有该控制权(也可能没有),只需在启动其他线程之前在
main
中创建每个单例的实例即可。那么根本就不要使用互斥体。那时你不会遇到线程问题,你可以使用规范的“不关心线程”版本:并且,是的,这确实会使你的代码对于那些无法关心线程的人来说更加危险。费心去阅读你的 API 文档,但是(IMNSHO)他们应得的一切:-)
No, you cannot even assume that
foo = p;
is atomic. It's possible that it might load 16 bits of a 32-bit pointer, then be swapped out before loading the rest.If another thread sneaks in at that point and calls
Instance()
, you're toasted because yourfoo
pointer is invalid.For true security, you will have to protect the entire test-and-set mechanism, even though that means using mutexes even after the pointer is built. In other words (and I'm assuming that
scoped_lock()
will release the lock when it goes out of scope here (I have little experience with Boost)), something like:If you don't want a mutex (for performance reasons, presumably), an option I've used in the past is to build all singletons before threading starts.
In other words, assuming you have that control (you may not), simply create an instance of each singleton in
main
before kicking off the other threads. Then don't use a mutex at all. You won't have threading problems at that point and you can just use the canonical don't-care-about-threads-at-all version:And, yes, this does make your code more dangerous to people who couldn't be bothered to read your API docs but (IMNSHO) they deserve everything they get :-)
为什么不保持简单呢?
编辑:在 C++11 中,线程被引入到语言中。以下是线程安全的。该语言保证实例仅在线程安全的情况下初始化一次。
所以它是懒惰地评估的。它的线程安全。非常简单。双赢/双赢/双赢。
Why not keep it simple?
Edit: In C++11 where threads is introduced into the language. The following is thread safe. The language guarantees that instance is only initialized once and in a thread safe manor.
So its lazily evaluated. Its thread safe. Its very simple. Win/Win/Win.
这取决于您使用的线程库。如果您使用的是 C++0x,则可以使用原子比较和交换操作以及写入屏障来保证双重检查锁定有效。如果您正在使用 POSIX 线程或 Windows 线程,您可能可以找到一种方法来做到这一点。更大的问题是为什么?事实证明,单例通常是不必要的。
This depends on what threading library you're using. If you're using C++0x you can use atomic compare-and-swap operations and write barriers to guarantee that double-checked locking works. If you're working with POSIX threads or Windows threads, you can probably find a way to do it. The bigger question is why? Singletons, it turns out, are usually unnecessary.
C++ 中的 new 运算符总是涉及两步过程:
1.) 分配与简单
malloc
相同的内存2.) 调用给定数据类型的构造函数
上面的代码将使单例创建分为3步,这甚至容易受到您试图解决的问题的影响。
the
new
operator in c++ always invovle 2-steps process :1.) allocating memory identical to simple
malloc
2.) invoke constructor for given data type
the code above will make the singleton creation into 3 step, which is even vulnerable to problem you trying to solve.
为什么不使用真正的互斥体来确保只有一个线程会尝试创建
foo
?这是一个带有免费读卡器的测试-测试-设置锁。如果您希望在非原子替换环境中保证读取安全,请将上面的内容替换为读写锁。
编辑:如果你确实想要免费读者,可以先写
foo
,然后写一个标志变量fooCreated = 1
。检查fooCreated != 0
是安全的;如果fooCreated != 0
,则foo
被初始化。Why don't you just use a real mutex ensuring that only one thread will attempt to create
foo
?This is a test-and-test-and-set lock with free readers. Replace the above with a reader-writer lock if you want reads to be guaranteed safe in a non-atomic-replacement environment.
edit: if you really want free readers, you can write
foo
first, and then write a flag variablefooCreated = 1
. CheckingfooCreated != 0
is safe; iffooCreated != 0
, thenfoo
is initialized.你的代码没有任何问题。在scoped_lock之后,该部分中将只有一个线程,因此第一个进入的线程将初始化foo并返回,然后第二个线程(如果有)进入,它将立即返回,因为foo不再为空。
编辑:粘贴简化的代码。
It has nothing wrong with your code. After the scoped_lock, there will be only one thread in that section, so the first thread that enters will initialize foo and return, and then second thread(if any) enters, it will return immediately because foo is not null anymore.
EDIT: Pasted the simplified code.
感谢您的所有意见。在查阅了 Joe Duffy 的优秀书籍后, “Windows上的并发编程”,我现在认为我应该使用下面的代码。除了一些重命名和 InterlockedXXX 行之外,大部分代码来自他的书中。以下实现
CPU。
所以,这应该是相当安全的(……对吧?):
Thanks for all your input. After consulting Joe Duffy's excellent book, "Concurrent Programming on Windows", I am now thinking that I should be using the code below. It's largely the code from his book, except for some renames and the InterlockedXXX line. The following implementation uses:
the CPU.
So, that should be pretty safe (... right?):