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

时间:2008-10-02 20:43:01

标签: c++ windows singleton

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

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 *'。 我应该将返回值修改为易失性Singleton *?

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

9 个答案:

答案 0 :(得分:13)

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

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

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

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

但是,有充分的理由避免单独使用单身人士(以及为什么他们有时被称为反模式):

  • 它们本质上是美化的全球变量
  • 它们可以导致应用程序的不同部分之间的高度耦合
  • 他们可以使单元测试变得更复杂或不可能(由于实际单例与虚假实现交换困难)

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

答案 1 :(得分:11)

如果您使用的是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。但是初始化很便宜,所以延迟初始化通常并不重要。

答案 2 :(得分:4)

虽然我喜欢已接受的解决方案,但我发现了另一个有前途的领导,并认为我应该在这里分享:One-Time Initialization (Windows)

答案 3 :(得分:1)

您可以使用OS原语(如互斥锁或临界区)来确保线程安全初始化,但是每次访问单例指针时(由于获取锁定)都会产生开销。它也不便携。

答案 4 :(得分:1)

这个问题需要考虑一个澄清点。你需要......

  1. 实际创建了一个且只有一个类的实例
  2. 可以创建类的许多实例,但只应该有一个真正的类
  3. 实例

    Web上有许多示例用于在C ++中实现这些模式。这是一个Code Project Sample

答案 5 :(得分:0)

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

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

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

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

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

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

在您的代码的最开始,您认为它是线程安全的,然后您不再懒惰初始化,在您的应用程序启动时,您将需要“一些”更多的处理能力。但是,如果你这样做,它将解决很多关于竞争条件的麻烦。

答案 6 :(得分:0)

如果您正在寻找更便携,更简单的解决方案,您可以转向提升。

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

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

答案 7 :(得分:0)

这个问题不要求单身人士是懒惰的。 由于许多答案都假设,我认为对于第一个短语讨论:

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

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

MSDN引用:“单个进程的线程可以使用关键节对象进行互斥同步。”。

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

进一步澄清:

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

现在,如果不要求“lazy-construct”,则以下解决方案既是跨模块安全的又是线程安全的,甚至是可移植的:

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(),请发表评论。

答案 8 :(得分:-1)

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

  • 你永远不应该使用