【问题标题】:Singleton instance declared as static variable of GetInstance method, is it thread-safe? [duplicate]单例实例声明为 GetInstance 方法的静态变量,它是线程安全的吗? [复制]
【发布时间】:2010-10-01 18:08:51
【问题描述】:

我已经看到了单例模式的实现,其中实例变量在 GetInstance 方法中被声明为静态变量。像这样:

SomeBaseClass &SomeClass::GetInstance()
{
   static SomeClass instance;
   return instance;
}

我看到了这种方法的以下积极方面:

  • 代码更简单,因为只有在第一次调用 GetInstance 时,编译器才负责创建这个对象。
  • 代码更安全,因为没有其他方法可以获取对实例的引用,而是使用 GetInstance 方法,并且没有其他方法可以更改实例,而是在 GetInstance 方法中。

这种方法有哪些负面影响(除了这不是非常面向 OOP)?这是线程安全的吗?

【问题讨论】:

    标签: c++ design-patterns singleton


    【解决方案1】:

    在 C++11 中它是线程安全的:

    §6.7 [stmt.dcl] p4 如果控制在变量初始化时同时进入声明,则并发执行应等待初始化完成。

    在 C++03 中:

    • 在 g++ 下它是线程安全的。
      但这是因为 g++ 显式添加了代码来保证它。

    一个问题是,如果您有两个单例,并且它们在构造和销毁过程中尝试相互使用。

    阅读: Finding C++ static initialization order problems

    这个问题的一个变种是如果单例是从全局变量的析构函数中访问的。在这种情况下,单例肯定已经被销毁了,但是 get 方法仍然会返回对被销毁对象的引用。

    有一些方法可以解决这个问题,但它们很混乱,不值得做。只是不要从全局变量的析构函数中访问单例。

    更安全但丑陋的定义:
    我相信你可以添加一些适当的宏来整理它

    SomeBaseClass &SomeClass::GetInstance()
    {
    #ifdef _WIN32 
    Start Critical Section Here
    #elif  defined(__GNUC__) && (__GNUC__ > 3)
    // You are OK
    #else
    #error Add Critical Section for your platform
    #endif
    
        static SomeClass instance;
    
    #ifdef _WIN32
    END Critical Section Here
    #endif 
    
        return instance;
    }
    

    【讨论】:

    • Does "§6.7 [stmt.dcl] p4 如果在初始化变量时控制同时进入声明,则并发执行应等待初始化完成。" ,这也适用于全局静态变量吗?谢谢
    • @Rick static 在全局变量上意味着不同的东西:变量有internal linkage。只有包含变量的翻译单元才能看到它并直接与之交互。请注意,如果您在多个翻译单元中具有相同的标识符,则它们都是具有相同名称的不同实例,static 可防止它们在链接器汇编程序时发生冲突。它只能在程序启动时初始化一次,因此线程不是问题。
    【解决方案2】:

    如图所示,它不是线程安全的。 C++ 语言对线程保持沉默,因此您无法从该语言获得内在保证。您将不得不使用平台同步原语,例如Win32 ::EnterCriticalSection(),保护访问。

    您的特定方法会出现问题 b/c 编译器将插入一些(非线程安全)代码以在第一次调用时初始化静态 instance,很可能是在函数体开始执行之前(因此之前可以调用任何同步。)

    使用指向SomeClass 的全局/静态成员指针,然后在同步块中进行初始化证明实现起来的问题较少。

    #include <boost/shared_ptr.hpp>
    
    namespace
    {
      //Could be implemented as private member of SomeClass instead..
      boost::shared_ptr<SomeClass> g_instance;
    }
    
    SomeBaseClass &SomeClass::GetInstance()
    {
       //Synchronize me e.g. ::EnterCriticalSection()
       if(g_instance == NULL)
         g_instance = boost::shared_ptr<SomeClass>(new SomeClass());
       //Unsynchronize me e.g. :::LeaveCriticalSection();
       return *g_instance;
    }
    

    我没有对此进行编译,因此仅用于说明目的。它还依赖于 boost 库来获得与原始示例相同的生命周期(或大约相同的生命周期)。你也可以使用 std::tr1 (C++0x)。

    【讨论】:

    • 是的,在 Windows 上使用临界区。 G++ 保证只有一个线程会初始化它。
    • 创建第二个私有方法 _getInstance(),它实际上包含静态实例的定义,然后让(公共)GetInstance() 方法在特定于操作系统的同步原语之间调用该方法。在这种情况下,C++ 无法重新排序,您可以避免堆分配。
    • @j_random_hacker:是的,这是个好主意。 @Martin York:感谢有关 g++ 的提示,我不知道。
    【解决方案3】:

    根据规范,这也应该在 VC++ 中工作。有谁知道吗?

    只需添加关键字 volatile。 如果 msdn 上的文档正确,则 Visual c++ 编译器应生成互斥锁。

    SomeBaseClass &SomeClass::GetInstance()
    {
       static volatile SomeClass instance;
       return instance;
    }
    

    【讨论】:

    【解决方案4】:

    它具有 Singleton 实现的所有常见缺陷,即:

    • It is untestable
    • 它不是线程安全的(这很简单,看看您是否想象两个线程同时进入函数)
    • 这是内存泄漏

    我建议不要在任何生产代码中使用 Singleton。

    【讨论】:

    • 这个问题与单例模式的使用无关,而是与它的特定实现有关。
    • 这个特定的实现有所有这些失败。请随时提出您喜欢的观点
    • 或者你知道,只是默默地投反对票。对我来说都是一样的
    • 实际上这在 G++ 下是线程安全的。编译器具有保证只有一个线程会初始化静态函数成员的逻辑。
    • 它在 C++ 中不是线程安全的,仅仅因为它在某些非标准实现中恰好是线程安全的并不会改变这一事实
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-08
    • 1970-01-01
    相关资源
    最近更新 更多