以下单例实现线程是否安全?

时间:2013-12-29 14:16:09

标签: c++ multithreading

#include<iostream>
using namespace std;
class singleton
{
      private:
      static singleton* ref;
      singleton()
      {
                 cout<<"singleton ctor"<<endl;
      }
      public:
      static singleton* getInstance()
      {
         return ref;
      }
};

singleton* singleton::ref=new singleton();

int main()
{

    singleton* ref=singleton::getInstance()
}

3 个答案:

答案 0 :(得分:3)

您的示例中没有线程。假设在静态初始化期间初始化的对象可能是产生线程并访问singleton::ref,则代码很容易访问未初始化的内存,并且它不是线程安全的。如果在输入main()后启动了第一个线程,则代码是线程安全的。

如果你想确保首次访问时对象是正确构造的,同时,即使在从多个线程进行静态初始化期间访问时,也要使构造线程安全,你可以使用

singleton* singleton::getInstance() {
    static singleton rc;
    return &rc;
}

标准中保证上述是线程安全的相关部分是6.7 [stmt.dcl]第4段:

  

...如果控件在初始化变量时同时进入声明,则并发执行应等待初始化完成...

第一次调用getInstance()时将构造对象。即使多个线程同时调用getInstance(),对象也只构造一次,并且对getInstance()的并发调用将阻塞,直到构造完成。您可以确保getInstance()的构造在静态初始化期间发生,方法是在singleton的实现中使用它:

static singleton* constructionDummy = singleton::getInstance();

请注意,单线程通常会导致重大问题,在多线程程序中则更是如此。除了单线程已经在单线程程序中创建的问题之外,它们还进一步引入了数据竞争的潜力,并且为了解决数据竞争,倾向于引入序列化。除了设置严格不可变数据的单例外,我建议使用它们。

答案 1 :(得分:0)

虽然作为一般原则,我同意这样的观点,即以明确定义的顺序创建对象是可取的,从线程安全的角度来看,问题中发布的实现比在getInstance中构造单例对象更安全。在后一种情况下,除非程序员明确地使用原子操作,理论上可以多次调用构造函数。以下是使用VS2013(无优化调试)对此代码的x86目标进行拆卸。

singleton* singleton::getInstance() {
    static singleton rc;
    return &rc;
} 

static singleton* getInstance()
{
    00E85820  push        ebp  
    00E85821  mov         ebp,esp  
    00E85823  push        0FFFFFFFFh  
    00E85825  push        0E89E6Eh  
    00E8582A  mov         eax,dword ptr fs:[00000000h]  
    00E85830  push        eax  
    00E85831  sub         esp,0C0h  
    00E85837  push        ebx  
    00E85838  push        esi  
    00E85839  push        edi  
    00E8583A  lea         edi,[ebp-0CCh]  
    00E85840  mov         ecx,30h  
    00E85845  mov         eax,0CCCCCCCCh  
    00E8584A  rep stos    dword ptr es:[edi]  
    00E8584C  mov         eax,dword ptr ds:[00E8F000h]  
    00E85851  xor         eax,ebp  
    00E85853  push        eax  
    00E85854  lea         eax,[ebp-0Ch]  
    00E85857  mov         dword ptr fs:[00000000h],eax  
    static singleton ref;
    00E8585D  mov         eax,dword ptr ds:[00E8F330h]  
    00E85862  and         eax,1  
    00E85865  jne         singleton::getInstance+6Ch (0E8588Ch)  
    00E85867  mov         eax,dword ptr ds:[00E8F330h]  
    00E8586C  or          eax,1  
    00E8586F  mov         dword ptr ds:[00E8F330h],eax  
    00E85874  mov         dword ptr [ebp-4],0  
    00E8587B  mov         ecx,0E8F32Ch  
    00E85880  call        singleton::singleton (0E810D2h)  
    00E85885  mov         dword ptr [ebp-4],0FFFFFFFFh  
    return &ref;
    00E8588C  mov         eax,0E8F32Ch  
}

dword ptr ds:[00E8F330h]用作标志来检查是否调用构造函数。

正如预期的那样,编译器不会发出原子比较和交换指令。因此,如果线程B在线程A执行指令00E8586F之前在00E8585D执行指令,则两者都将调用构造函数。但是,为了在实践中看到这一点,我们可能需要使用特殊的测试用例,其中多个线程被生成(在多核处理器上)并在事件/信号量上被阻塞。然后发信号通知信号量立刻释放所有线程,希望我们将看到构造函数将被多次调用。如果在getInstance中执行新的singleton(),则同样适用。

此问题并非特定于单身人士。

答案 2 :(得分:-1)

如果你的意思是线程安全,不同的线程会通过调用singleton::getInstance得到正确的(相同的)指针,而不是:是的,它是线程安全的。

并且(正如评论中所指出的那样)你应该在main函数(或另一个函数)中以明确定义的顺序创建所有非平凡的静态变量!

所以写下来:

singleton* singleton::ref = NULL;

int main()
{
    singleton::ref = new singleton();
    // create objects which use the singleton
    singleton* ref = singleton::getInstance();
    // create threads...

}