我看到了很多关于C++双重检查锁定的文章,常用于防止多个线程尝试初始化懒惰创建的单例对象。但现在有文章称其存在问题。正常的双重检查锁定代码如下:
class singleton {
private:
singleton(); // private constructor so users must call instance()
static boost::mutex _init_mutex;
public:
static singleton & instance()
{
static singleton* instance;
if(!instance)
{
boost::mutex::scoped_lock lock(_init_mutex);
if(!instance)
instance = new singleton;
}
return *instance;
}
};
问题似乎在于分配实例的那一行--编译器可以自由地分配对象,然后将指针分配给它,或者将指针设置为将要分配的位置,然后再分配它。后一种情况会破坏单例模式--一个线程可能会分配内存并分配指针,但在将其放入睡眠状态之前不运行单例构造函数--然后第二个线程将看到实例不为 null 并尝试返回它,即使它还没有被构造。
我看到一个建议使用一个线程本地布尔值来检查而不是 instance
。类似这样:
class singleton {
private:
singleton(); // private constructor so users must call instance()
static boost::mutex _init_mutex;
static boost::thread_specific_ptr<int> _sync_check;
public:
static singleton & instance()
{
static singleton* instance;
if(!_sync_check.get())
{
boost::mutex::scoped_lock lock(_init_mutex);
if(!instance)
instance = new singleton;
// Any non-null value would work, we're really just using it as a
// thread specific bool.
_sync_check = reinterpret_cast<int*>(1);
}
return *instance;
}
};
这样每个线程最终都会检查实例是否已经被创建了一次,但是在此之后就停止了,这会带来一些性能损失,但仍然远不及每次调用时都加锁的情况那么糟糕。但是如果我们只使用本地静态bool变量呢?
class singleton {
private:
singleton(); // private constructor so users must call instance()
static boost::mutex _init_mutex;
public:
static singleton & instance()
{
static bool sync_check = false;
static singleton* instance;
if(!sync_check)
{
boost::mutex::scoped_lock lock(_init_mutex);
if(!instance)
instance = new singleton;
sync_check = true;
}
return *instance;
}
};
为什么这样行不通?即使sync_check被一个线程读取时另一个线程正在对其进行赋值,垃圾值仍然不为零,因此为真。这篇Dr. Dobb's文章声称你必须锁定,因为你永远不会在重排序指令上与编译器争斗。这让我想到出于某种原因这个方法肯定不起作用,但我想不出为什么。如果序列点的要求像Dr. Dobb's文章让我相信的那样宽松,我就不明白为什么锁之后的任何代码都不能被重新排序到锁之前。这将使C++多线程期间失效。
我猜编译器允许特别地将sync_check重新排序到锁之前,因为它是一个本地变量(尽管它是静态的,我们没有返回引用或指针)——但这仍然可以通过将其变为静态成员(实际上是全局的)来解决。
所以这个方法能行得通吗?为什么?
sync_check = true;
语句之前添加特定于平台的内存屏障指令,例如 Windows 上的_ReadWriteBarrier()
(http://msdn.microsoft.com/en-us/library/f20w0x5e%28VS.80%29.aspx),则您最后的示例将可行。此外,从同一篇文章中可以看出,对于该编译器,自 VS2003 起,只需将sync_check
声明为volatile
即可解决问题。 - Praetorian