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

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

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

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

我读过这个帖子:

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

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

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

这可以实现我想要的吗?

编辑: 我想要惰性初始化,并且该类永远只有一个实例。

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

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

编辑: 除了 return 语句之外,代码工作正常 - 我收到错误:错误 C2440:'返回' :无法从“易失性单例*”转换为“单例*”。我应该将返回值修改为 volatile Singleton * 吗?

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

有帮助吗?

解决方案

如果您使用的是Visual C ++ 2005/2008,则可以使用双重检查锁定模式,因为<!>“ volatile变量表现为围栏 <!>”;这是实现延迟初始化单例的最有效方法。

来自 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的语义,使其成为可能。

您必须声明并初始化关键部分代码中的其他地方。但是初始化很便宜,所以延迟初始化通常并不重要。

其他提示

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

然后通过互斥体/临界区以通常的方式实现对单例的线程安全访问。

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

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

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

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

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

虽然我喜欢接受的解决方案,但我发现了另一个有希望的领导者,并认为我应该在这里分享:一次性初始化(Windows)

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

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

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

    Web上有许多示例用于在C ++中实现这些模式。这是代码项目示例

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

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

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

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

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

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

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

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

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

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

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

鉴于语言本身不是线程感知,加上优化技术,编写可移植的可靠c ++单例非常困难(如果不是不可能),请参阅<!>“ C ++和双重锁定的危险 <!> quot;作者:Scott Meyers和Andrei Alexandrescu。

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

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

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分配给正确的值。由于这个事实,它也不是惰性构造的),在这个表达式中,逗号是一个运算符,而不是标点符号。

明确使用<!> quot; extern <!> quot;这里为了防止编译器优化它(关注Scott Meyers的文章,大敌是优化器。),还要制作静态分析工具,如pc-lint保持沉默。 <!>;在main / DllMain <!>之前; Scott meyer称为<!>“单线程启动部分<!>”; in <!> quot; Effective C ++ 3rd <!> quot;第4项。

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

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

  • 你永远不应该使用它
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top