16

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

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

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

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

我在 SO 上读过这个帖子:

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

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

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

这可以做我想要的吗?

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

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

接受的答案:
当我使用 Visual Studio 2008 时,我 接受了Josh 的回答- 注意:对于未来的读者,如果您不使用此编译器(或 2005) - 不要使用接受的答案!

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

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

4

9 回答 9

13

保证单例的跨平台线程安全初始化的一种简单方法是在应用程序启动任何其他线程之前(或至少任何其他将访问单例的线程)。

然后以通常的方式使用互斥锁/临界区来确保对单例的线程安全访问。

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

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

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

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

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

于 2008-10-02T20:51:04.913 回答
11

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

于 2008-10-02T21:12:58.903 回答
4

虽然我喜欢公认的解决方案,但我刚刚找到了另一个有希望的线索,并认为我应该在这里分享它:一次性初始化 (Windows)

于 2012-04-20T20:12:12.463 回答
1

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

于 2008-10-02T20:51:37.143 回答
1

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

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

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

于 2008-10-02T20:53:23.210 回答
0

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

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

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

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

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

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

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

于 2008-10-02T21:01:29.133 回答
0

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

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

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

于 2008-10-21T19:40:22.947 回答
0

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

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

我已经看到许多答案通过使用 CriticalSection 在 Windows 平台上同步对象,但是当所有线程都在一个处理器上运行时,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);

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

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

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

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

于 2012-06-21T05:24:21.677 回答
-1

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

  • 你不应该使用它
于 2008-10-02T20:48:06.293 回答