5

所以我见过很多文章声明在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并尝试返回它,尽管它还没有建成。

saw a suggestion使用线程本地布尔值,并检查,而不是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被另一个线程在另一个线程中分配时读取,垃圾值仍然不为零,因此也是如此。 This Dr. Dobb's article声称你必须锁定,因为你永远不会因为重新排序指令而与编译器争斗。这让我觉得这不应该出于某种原因,但我不明白为什么。如果序列点的要求像Dobb博士的文章让我相信的那样丢失,我不明白为什么锁之后的任何代码都不能重新排序为在锁之前。这将使C++多线程断开期间。

我想我可以看到编译器被允许特别重新排序sync_check在锁之前,因为它是一个局部变量(即使它是静态的,我们没有返回一个引用或指针) - 但是这样仍然可以通过使其成为静态成员(有效全局)来解决。

那么这项工作还是不会呢?为什么?

+2

问题是变量可能在构造函数运行(或完成)之前分配,而不是在分配对象之前分配。 – kdgregory 2009-06-03 14:53:41

+0

谢谢,纠正。我完全误解了比赛的状况。 – 2009-06-03 15:21:57

+1

是的,你是对的,现在的C++确实是“多线程断点”。只考虑标准。编译器供应商通常会提供解决方法,因此实际结果并不那么糟糕。 – Suma 2009-06-16 15:58:27

回答

5

您修复不能解决任何事情,因为在写入sync_check和实例都可以做出来的顺序在CPU上。举个例子,前两个实例调用大概同时发生在两个不同的CPU上。第一个线程将获取锁,初始化指针并将sync_check设置为true,但处理器可能会改变写入内存的顺序。在另一个CPU上,第二个线程可能会检查sync_check,看看它是否为真,但实例可能尚未写入内存。详细信息请参见Lockless Programming Considerations for Xbox 360 and Microsoft Windows

你提到的应该工作,那么线程特定sync_check解决方案(假设你初始化指针为0)。

+0

关于你的最后一句话:是的,但我不确定,但我认为thread_specific_ptr在内部使用互斥锁。那么使用这个解决方案与只是总是锁定互斥体(没有双重锁定)有什么关系呢? – n1ckp 2010-06-09 19:28:38

1

有一个关于这一些伟大的阅读(虽然它的.NET/C#面向)位置:http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

什么它归结为是,你需要能够告诉它不能重新排序CPU的读/写对于这种可变访问(自从原来的Pentium以来,如果CPU认为逻辑不受影响,CPU可以对某些指令进行重新排序),并且需要确保缓存一致(不要忘记 - 我们的开发者假设所有内存只是一个平面资源,但实际上,每个CPU内核都有缓存,一些未共享(L1),有些可能有时会共享(L2)) - 您的初始化可能会写入主RAM,但另一个内核缓存中可能有未初始化的值。如果你没有任何并发​​语义,CPU可能不知道它的缓存是脏的。

我不知道C++方面,但在.net中,你会指定变量为volatile以保护对它的访问(或者你可以使用System.Threading中的内存读/写屏障方法)。另外,我已经阅读.net 2.0,双重检查锁定保证没有“易变”变量(对于任何.net读者) - 这不会帮助你与你的C++码。

如果你想安全,你需要做标记一个变量在C#中挥发性的C++等价的。

+1

C++变量可以被声明为volatile,但我怀疑它具有与C#完全相同的语义。我还记得在某处读到这是一种滥用波动的情况,但我不记得为什么我不能判断这篇文章是如何理由的。 – 2009-06-03 15:57:57

+0

在不同的语言中,这可能是一种滥用(甚至可能是滥用C#)。编写低锁或无锁代码的一个非常困难的方面是指导上的差异。我已经花时间阅读了这篇文章,而且似乎即使在微软内部,一些博客似乎在需要内存围栏时以及何时使用易失性内容方面与另一方矛盾。可以肯定的是,这是一个难题。 – JMarsch 2009-06-03 16:06:09

+0

在当前的C++中没有相当于.NET的volatile(按标准定义)。这是C++ 0x标准即将出台的领域之一。同时你需要使用你的编译器提供的东西(在Visual Studio中是指volatile和内存围栏)。 – Suma 2009-06-16 15:56:00

0

“后一种情况打破了习惯用语 - 两个线程可能最终创建单身人士。”

但是,如果我正确理解了代码,第一个示例中,您检查实例是否已经存在(可能会同时由多个线程执行),如果它没有一个线程被锁定,实例 - 当时只有一个线程可以执行创建。所有其他线程被锁定并等待。

一旦创建了实例,并互斥被解锁下一个等待线程将锁定互斥体,但它不会尝试创建新实例,因为该检查将失败。

下一次实例变量被选中时,它将被设置,所以没有线程会尝试创建新的实例。

我不知道在哪里,而另一个线程检查同一变量一个线程分配新的实例指针实例的情况下 - 但我相信它会正确地在这种情况下进行处理。

我在这里错过了什么吗?

好了不知道操作的重新排序,但在这种情况下,它会改变逻辑,所以我不希望这样的情况发生 - 但我对这个话题没有专家。

+0

你是对的 - 我对实际的竞争条件错了。问题是第二个线程可能会看到实例非空,并尝试在第一个线程构建它之前返回它。我编辑了我的帖子。 – 2009-06-03 15:22:53