代码之家  ›  专栏  ›  技术社区  ›  Mark Ingram

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

  •  16
  • Mark Ingram  · 技术社区  · 16 年前

    我在这里读过线程安全的单例模式:

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

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

    那是 只有 保证线程安全初始化的方法?

    我读过这篇文章,所以:

    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将删除易失性限定符。再次感谢乔希。

    9 回复  |  直到 13 年前
        1
  •  11
  •   Salman A    16 年前

    如果使用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);
    }
    

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

    在任何编译器上使用这个,因为它不可移植。该标准没有保证这将如何工作。VisualC++ 2005明确地增加了易失性的语义,使之成为可能。

    你得申报 initialize the CRITICAL SECTION 代码中的其他地方。但这种初始化很便宜,所以延迟初始化通常并不重要。

        2
  •  13
  •   community wiki 8 revs Matthew Murdoch    16 年前

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

    然后,使用互斥锁/关键部分,以通常的方式确保线程安全地访问singleton。

    延迟初始化 也可以使用类似的机制来实现。遇到的常见问题是,提供线程安全性所需的互斥体通常是在单实例本身中初始化的,这只会将线程安全问题推到互斥体/关键部分的初始化。解决这个问题的一种方法是在应用程序的主线程中创建和初始化互斥/关键部分,然后通过调用静态成员函数将其传递给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
            }
    };
    

    然而,有充分的理由避免使用单件(以及为什么有时它们被称为 反模式 ):

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

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

        3
  •  4
  •   TripShock    13 年前

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

        4
  •  1
  •   Henk    16 年前

    您可以使用操作系统原语(如mutex或critical部分)来确保线程安全初始化,但是每次访问singleton指针时(由于获取锁),这都会导致开销。它也是不便携的。

        5
  •  1
  •   JaredPar    16 年前

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

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

    在Web上有很多示例在C++中实现这些模式。这里有一个 Code Project Sample

        6
  •  0
  •   Eric    16 年前

    下面解释了如何在C中完成这项工作,但完全相同的概念适用于任何支持单例模式的编程语言。

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

    您需要决定的是您是否需要延迟初始化。惰性初始化意味着在第一次调用singleton时就创建了它内部包含的对象。 前任:

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

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

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

    在代码的开头,假设它是线程安全的,然后您就不再懒惰地初始化了,当应用程序启动时,您将需要“一些”更多的处理能力。但是,如果你这样做的话,它会解决很多关于比赛条件的难题。

        7
  •  0
  •   mmocny    16 年前

    如果你正在寻找一个更便携、更简单的解决方案,你可以转向Boost。

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

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

        8
  •  0
  •   zhaorufei    13 年前

    这个问题不需要单例是懒惰构造还是不需要。 由于许多答案都是假设的,所以我假设第一个短语讨论:

    鉴于语言本身不是线程意识,加上优化技术,编写一个可移植的可靠C++单元格是非常困难的(如果不是不可能的话),请参见 C++ and the Perils of Double-Checked Locking 作者:斯科特·迈尔斯和安德烈·亚历山大·斯库。

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

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

    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 /dLmain中,Scott meyer被称为“单线程启动部分”,在“有效C++第三”项4中。

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

        9
  •  -1
  •   1800 INFORMATION    16 年前

    有很多方法可以在Windows上执行线程安全的singleton*初始化。事实上,其中一些甚至是跨平台的。在你链接到的so线程中,他们正在寻找一个用c懒散地构造的单例,这有点具体,而且考虑到你正在使用的内存模型的复杂性,做正确的工作可能会更复杂一些。

    • 你不应该用它