【问题标题】:How can I create a thread-safe singleton pattern in Windows?如何在 Windows 中创建线程安全的单例模式?
【发布时间】:2010-09-14 22:28:57
【问题描述】:

我一直在这里阅读有关线程安全的单例模式:

http://en.wikipedia.org/wiki/Singleton_pattern#C.2B.2B_.28using_pthreads.29

它在底部说唯一安全的方法是使用 pthread_once - 这在 Windows 上不可用。

这是保证线程安全初始化的唯一方式吗?

我在 SO 上读过这个帖子:

Thread safe lazy construction of a singleton in C++

并且似乎暗示了原子操作系统级别的交换和比较功能,我假设在 Windows 上是:

http://msdn.microsoft.com/en-us/library/ms683568.aspx

这可以做我想做的事吗?

编辑:我想要延迟初始化并且永远只有一个类的实例。

另一个站点上的某个人提到在命名空间内使用全局变量(他将单例描述为反模式) - 它怎么可能是“反模式”?

接受的答案:
我已接受 Josh's answer,因为我正在使用 Visual Studio 2008 - 注意:对于未来的读者,如果您不使用此编译器(或 2005) - 不要使用已接受的答案!!

编辑: 代码工作正常,除了 return 语句 - 我得到一个错误: 错误 C2440:“return”:无法从“volatile Singleton *”转换为“Singleton *”。 我应该将返回值修改为 volatile Singleton *?

编辑:显然 const_cast 将删除 volatile 限定符。再次感谢乔希。

【问题讨论】:

  • 在创建任何线程之前初始化 singelton。
  • Singelton 是一种反模式,因为它大多使用不正确。即作为全局变量的替代品。
  • 全局变量不只是 C 语言的一个令人讨厌的回归吗?
  • 如何在 Josh 的回答中初始化 CRITICAL_SECTION 'cs'?
  • 关于如何在 C++ 中实现单例以及线程安全的精彩讨论可以在这篇论文中找到:aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf

标签: c++ windows singleton


【解决方案1】:

有很多方法可以在 Windows 上进行线程安全的 Singleton* 初始化。事实上,其中一些甚至是跨平台的。在您链接到的 SO 线程中,他们正在寻找一个在 C 中延迟构建的 Singleton,考虑到您正在处理的内存模型的复杂性,这更具体,并且可能有点棘手。 .

  • 你不应该使用它

【讨论】:

    【解决方案2】:

    保证单例的跨平台线程安全初始化的简单方法是在应用程序的主线程中显式执行(通过调用单例上的静态成员函数)在您的应用程序启动任何其他线程(或至少任何其他将访问单例的线程)。

    然后使用互斥体/关键部分以通常的方式实现对单例的线程安全访问。

    延迟初始化也可以使用类似的机制来实现。遇到的常见问题是提供线程安全所需的互斥锁通常在单例本身中初始化,这只会将线程安全问题推向互斥锁/关键部分的初始化。解决此问题的一种方法是在应用程序的主线程中创建和初始化互斥锁/临界区,然后通过调用静态成员函数将其传递给单例。然后可以使用这个预初始化的互斥体/临界区以线程安全的方式对单例进行重量级初始化。例如:

    // A critical section guard - create on the stack to provide 
    // automatic locking/unlocking even in the face of uncaught exceptions
    class Guard {
        private:
            LPCRITICAL_SECTION CriticalSection;
    
        public:
            Guard(LPCRITICAL_SECTION CS) : CriticalSection(CS) {
                EnterCriticalSection(CriticalSection);
            }
    
            ~Guard() {
                LeaveCriticalSection(CriticalSection);
            }
    };
    
    // A thread-safe singleton
    class Singleton {
        private:
            static Singleton* Instance;
            static CRITICAL_SECTION InitLock;
            CRITICIAL_SECTION InstanceLock;
    
            Singleton() {
                // Time consuming initialization here ...
    
                InitializeCriticalSection(&InstanceLock);
            }
    
            ~Singleton() {
                DeleteCriticalSection(&InstanceLock);
            }
    
        public:
            // Not thread-safe - to be called from the main application thread
            static void Create() {
                InitializeCriticalSection(&InitLock);
                Instance = NULL;
            }
    
            // Not thread-safe - to be called from the main application thread
            static void Destroy() {
                delete Instance;
                DeleteCriticalSection(&InitLock);
            }
    
            // Thread-safe lazy initializer
            static Singleton* GetInstance() {
                Guard(&InitLock);
    
                if (Instance == NULL) {
                    Instance = new Singleton;
                }
    
                return Instance;
            }
    
            // Thread-safe operation
            void doThreadSafeOperation() {
                Guard(&InstanceLock);
    
                // Perform thread-safe operation
            }
    };
    

    但是,有充分的理由完全避免使用单例(以及为什么有时将它们称为反模式):

    • 它们本质上是美化的全局变量
    • 它们会导致应用程序不同部分之间的高度耦合
    • 它们可以使单元测试变得更加复杂或不可能(因为很难将真正的单例与假实现交换)

    另一种方法是使用“逻辑单例”,您可以在主线程中创建和初始化一个类的单个实例,并将其传递给需要它的对象。如果您想将许多对象创建为单例,这种方法可能会变得笨拙。在这种情况下,可以将不同的对象捆绑到单个“上下文”对象中,然后在必要时传递。

    【讨论】:

      【解决方案3】:

      您可以使用诸如互斥锁或临界区之类的操作系统原语来确保线程安全初始化,但这将在每次访问您的单例指针时产生开销(由于获取锁)。它也不便携。

      【讨论】:

        【解决方案4】:

        对于这个问题,您需要考虑一个澄清点。你需要...

        1. 实际上创建了一个类的一个也是唯一一个实例
        2. 可以创建一个类的许多实例,但该类应该只有一个真正确定的实例

        网络上有许多示例可以在 C++ 中实现这些模式。这是Code Project Sample

        【讨论】:

        • 第一个(只能创建一个类)
        【解决方案5】:

        以下解释了如何在 C# 中执行此操作,但完全相同的概念适用于任何支持单例模式的编程语言

        http://www.yoda.arachsys.com/csharp/singleton.html

        您需要决定是否要延迟初始化。延迟初始化意味着包含在单例中的对象是在第一次调用它时创建的 例如:

        MySingleton::getInstance()->doWork();
        

        如果直到稍后才进行该调用,则线程之间存在竞争条件的危险,如文章中所述。但是,如果你把

        MySingleton::getInstance()->initSingleton();
        

        在您假设它是线程安全的代码的最开始,您不再延迟初始化,当您的应用程序启动时您将需要“一些”更多的处理能力。但是,如果您这样做,它将解决很多关于比赛条件的问题。

        【讨论】:

        • 我不认为“完全相同的概念适用于任何编程语言”,对于 C# 这很容易,该语言保证静态 ctor 只会被调用一次并且是线程安全的。对于 C++,这很困难。
        【解决方案6】:

        如果您使用的是 Visual C++ 2005/2008,则可以使用双重检查锁定模式,因为“volatile variables behave as fences”。这是实现延迟初始化单例的最有效方式。

        来自MSDN Magazine:

        Singleton* GetSingleton()
        {
            volatile static Singleton* pSingleton = 0;
        
            if (pSingleton == NULL)
            {
                EnterCriticalSection(&cs);
        
                if (pSingleton == NULL)
                {
                    try
                    {
                        pSingleton = new Singleton();
                    }
                    catch (...)
                    {
                        // Something went wrong.
                    }
                }
        
                LeaveCriticalSection(&cs);
            }
        
            return const_cast<Singleton*>(pSingleton);
        }
        

        当您需要访问单例时,只需调用 GetSingleton()。第一次调用时,静态指针将被初始化。初始化后,NULL检查将阻止仅读取指针的锁定。

        请勿在任何编译器上使用它,因为它不可移植。该标准不保证这将如何工作。 Visual C++ 2005 显式添加了 volatile 的语义以使这成为可能。

        您必须在代码的其他地方声明和initialize the CRITICAL SECTION。但是这种初始化很便宜,所以惰性初始化通常并不重要。

        【讨论】:

        • 这不起作用:阅读这篇文章erdani.org/publications/DDJ_Jul_Aug_2004_revised.pdf
        • 此代码不是异常安全的:如果异常开始出现,将不会调用 LeaveCriticalSection()。
        • 乔希是正确的。需要注意的是,此处使用的 'volatile' 是 Visual C++ 特定的,如果代码使用不同的 Windows 编译器(Intel、gcc 等)编译或移植到其他操作系统,则无法确保线程安全。
        • CRITICAL_SECTION 'cs' 是如何初始化的?
        • 如何保证在调用 GetSingleton 之前初始化 CRITICAL_SECTION?
        【解决方案7】:

        如果您正在寻找更便携、更简单的解决方案,您可以求助于 boost。

        boost::call_once 可用于线程安全初始化。

        它使用起来非常简单,并将成为下一个 C++0x 标准的一部分。

        【讨论】:

          【解决方案8】:

          虽然我喜欢公认的解决方案,但我刚刚找到了另一个有希望的潜在客户,并认为我应该在这里分享它:One-Time Initialization (Windows)

          【讨论】:

            【解决方案9】:

            这个问题不要求单例是否是惰性构造的。 由于许多答案都假设,我假设对于第一句话讨论:

            鉴于语言本身不是线程感知的,再加上优化技术,编写可移植的可靠 c++ 单例非常困难(如果不是不可能的话),请参阅 Scott Meyers 和 Andrei Alexandrescu 的“C++ and the Perils of Double-Checked Locking”。

            我已经看到许多答案在 windows 平台上通过使用 CriticalSection 来同步对象,但 CriticalSection 仅当所有线程都在一个处理器上运行时才是线程安全的,今天可能不是这样。

            MSDN 引用:“单个进程的线程可以使用临界区对象进行互斥同步。”。

            还有http://msdn.microsoft.com/en-us/library/windows/desktop/ms682530(v=vs.85).aspx

            进一步澄清:

            临界区对象提供的同步类似于互斥对象提供的同步,但临界区只能由单个进程的线程使用。

            现在,如果“惰性构造”不是必需的,那么以下解决方案既是跨模块安全的,也是线程安全的,甚至是可移植的:

            struct X { };
            
            X * get_X_Instance()
            {
                static X x;
                return &x;
            }
            extern int X_singleton_helper = (get_X_instance(), 1);
            

            它是跨模块安全的,因为我们使用本地范围的静态对象而不是文件/命名空间范围的全局对象。

            它是线程安全的,因为:在进入 main 或 DllMain 之前必须将 X_singleton_helper 分配给正确的值。也因为这个事实,它不是惰性构造的),在这个表达式中,逗号是一个运算符,而不是标点符号。

            在这里显式使用“extern”来防止编译器对其进行优化(关注Scott Meyers 的文章,最大的敌人是优化器。),同时也让pc-lint 等静态分析工具保持沉默。 “在 main/DllMain 之前”是 Scott meyer 在“Effective C++ 3rd”第 4 项中称为“单线程启动部分”。

            但是,我不太确定是否允许编译器根据语言标准优化 get_X_instance() 的调用,请评论。

            【讨论】:

            • 我必须佩服我对处理器和进程的混淆是多么愚蠢。关键当然可以用于多处理器/多核硬件,只要在一个进程内。我没有删除上面错误的部分,而是将其保留在这里并使用此评论来提醒自己我的错误。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-07-27
            • 2014-06-13
            • 2014-01-28
            • 2017-04-29
            相关资源
            最近更新 更多