如何在Windows中创建线程安全的单例模式?

时间:2020-03-06 15:02:58  来源:igfitidea点击:

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

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

它在底部说,唯一安全的方法是使用Windows无法使用的pthread_once。

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

我已经在SO上阅读了该线程:

C ++中单例的线程安全惰性构造

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

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

这可以做我想要的吗?

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

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

接受的答案:
我在使用Visual Studio 2008 NB时接受了乔什的答案:对于将来的读者,如果我们不使用此编译器(或者2005),请不要使用接受的答案!!

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

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

解决方案

有很多方法可以在Windows上进行线程安全的Singleton *初始化。实际上,其中一些甚至是跨平台的。在我们链接到的SO线程中,他们正在寻找用C惰性构造的Singleton,这更加具体,并且考虑到我们正在使用的内存模型的复杂性,可能做起来有些棘手。 。

  • 你永远不应该使用

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

然后,通过互斥体/关键部分以通常的方式来确保线程对单例的安全访问。

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

// 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
        }
};

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

  • 它们本质上是荣耀的全局变量
  • 它们可能导致应用程序不同部分之间的高度耦合
  • 它们会使单元测试变得更加复杂或者不可能(由于很难用伪造的实现交换真实的单例)

一种替代方法是利用"逻辑单例",在主线程中创建和初始化类的单个实例,然后将其传递给需要它的对象。当我们要创建许多对象作为单例对象时,这种方法可能会变得笨拙。在这种情况下,可以将不同的对象捆绑为一个"上下文"对象,然后在必要时将其传递出去。

我们可以使用互斥锁或者关键部分之类的OS原语来确保线程安全初始化,但是每次访问单例指针时(由于获取锁),这都会产生开销。它也是非便携式的。

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

  • 实际上曾经创建过一个类的一个实例
  • 可以创建一个类的许多实例,但是应该只有一个真正的该类的确定实例

网上有很多示例可以在C ++中实现这些模式。这是一个代码项目示例

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

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

我们需要决定是否要进行延迟初始化。延迟初始化意味着单例内部包含的对象是在第一次调用它时创建的
前任 :

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

如果直到稍后才进行该调用,则存在如文章中所述的线程之间竞争状态的危险。但是,如果我们放

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

在我们认为代码是线程安全的代码的开始,那么我们就不再需要懒惰的初始化了,当应用程序启动时,我们将需要更多的处理能力。但是,如果我们这样做的话,它将解决很多有关比赛条件的麻烦。

如果我们使用的是Visual C ++ 2005/2008,则可以使用经过双重检查的锁定模式,因为"易失性变量的行为就像篱笆"。这是实现延迟初始化的单例的最有效方法。

从《 MSDN杂志》:

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的语义,以实现此目的。

我们必须在代码的其他地方声明和初始化CRITICAL SECTION。但是初始化很便宜,因此延迟初始化通常并不重要。

如果我们正在寻找一种更轻便,更简单的解决方案,则可以寻求增强。

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

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