因此,我现在看到很多文章声称在 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;
}
};
问题显然是行分配实例——编译器可以自由地分配对象,然后分配指针给它,或者将指针设置到它将被分配的位置,然后分配它。后一种情况打破了惯用语——一个线程可以分配内存并分配指针,但在它进入睡眠状态之前不运行单例的构造函数——然后第二个线程将看到实例不为空并尝试返回它,即使它尚未构建。
我看到一个建议使用线程本地布尔值并检查它而不是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;
}
};
这样,每个线程最终都会检查实例是否已创建一次,但在此之后停止,这会导致一些性能损失,但仍然没有锁定每个调用那么糟糕。但是如果我们只使用本地静态布尔值呢?
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 在被分配到另一个线程时被一个线程读取,垃圾值仍然是非零的,因此为真。Dobb 博士的这篇文章声称您必须锁定,因为您永远不会在重新排序指令上与编译器进行战斗。这让我认为由于某种原因这一定行不通,但我不知道为什么。如果对序列点的要求像 Dobb 博士的文章让我相信的那样丢失,我不明白为什么锁之后的任何代码都不能重新排序到锁之前。这将使 C++ 多线程中断期。
我想我可以看到编译器被允许专门将sync_check重新排序到锁之前,因为它是一个局部变量(即使它是静态的,我们也没有返回指向它的引用或指针)——但这仍然可以解决通过使其成为静态成员(实际上是全局的)来代替。
那么这会起作用还是不会呢?为什么?