C ++中单例的线程安全延迟构造

时间:2008-08-09 21:16:29

标签: c++ multithreading singleton construction lazy-initialization

有没有办法在C ++中实现单例对象:

  1. 以线程安全方式构造Lazily(两个线程可能同时是单例的第一个用户 - 它应该仍然只构造一次)。
  2. 不依赖于事先构造的静态变量(因此单例对象本身在构造静态变量时可以安全使用)。
  3. (我不太了解我的C ++,但是在执行任何代码之前初始化积分和常量静态变量的情况(即,甚至在执行静态构造函数之前 - 它们的值可能已经“初始化”)在程序图像中)?如果是这样 - 也许这可以被利用来实现单例互斥体 - 这反过来可以用来保护真正的单例的创建..)


    很好,现在我似乎有几个好的答案(遗憾的是我不能将2或3标记为答案)。似乎有两个广泛的解决方案:

    1. 使用POD静态变量的静态初始化(而不是动态初始化),并使用内置原子指令实现我自己的互斥锁。这是我在问题中暗示的那种解决方案,我相信我已经知道了。
    2. 使用其他库函数,例如pthread_onceboost::call_once。这些我当然不知道 - 并且非常感谢所发布的答案。

9 个答案:

答案 0 :(得分:13)

不幸的是,Matt的答案包含了所谓的双重检查锁定,它不受C / C ++内存模型的支持。 (它受Java 1.5及更高版本支持 - 我认为是.NET内存模型。)这意味着在pObj == NULL检查发生的时间和获取锁定(互斥锁)之间{{1可能已经在另一个线程上分配了。线程切换在操作系统需要时发生,而不是在程序的“行”之间发生(在大多数语言中没有意义的后编译)。

此外,正如Matt所承认的,他使用pObj作为锁而不是OS原语。不要那样做。正确的锁需要使用内存屏障指令,潜在的缓存行刷新等等;使用您的操作系统的原语进行锁定。这一点尤其重要,因为使用的原语可以在运行操作系统的各个CPU行之间进行更改;什么在CPU Foo上工作可能不适用于CPU Foo2。大多数操作系统本身支持POSIX线程(pthreads)或提供它们作为OS线程包的包装器,因此通常最好用它们来说明示例。

如果您的操作系统提供了适当的原语,并且您绝对需要它来提高性能,那么您可以使用原子比较和交换操作来初始化共享全局,而不是进行此类锁定/初始化变量。基本上,你写的东西看起来像这样:

int

这只有在创建单个实例的多个实例(每个线程一个同时调用GetSingleton()时)安全,然后抛出额外内容时才有效。 Mac OS X上提供的MySingleton *MySingleton::GetSingleton() { if (pObj == NULL) { // create a temporary instance of the singleton MySingleton *temp = new MySingleton(); if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) { // if the swap didn't take place, delete the temporary instance delete temp; } } return pObj; } 函数 - 大多数操作系统都提供了类似的原语 - 检查OSAtomicCompareAndSwapPtrBarrier是否为pObj,并且只有NULL才真正将其设置为temp 。这使用硬件支持,实际上只执行交换一次并判断它是否发生。

如果您的操作系统提供的介于两个极端之间的另一个工具是pthread_once。这使您可以设置一个仅运行一次的功能 - 基本上通过执行所有锁定/屏障/等操作。诡计 - 无论调用多少次或调用多少线程。

答案 1 :(得分:11)

基本上,您要求同步创建单例,而不使用任何同步(先前构造的变量)。一般来说,不,这是不可能的。您需要可用于同步的内容。

至于你的另一个问题,是的,可以保证在执行其他代码之前初始化可以静态初始化的静态变量(即不需要运行时代码)。这使得可以使用静态初始化的互斥锁来同步单例的创建。

从2003年版的C ++标准开始:

  

具有静态存储持续时间(3.7.1)的对象应在进行任何其他初始化之前进行零初始化(8.5)。使用常量表达式进行零初始化和初始化统称为静态初始化;所有其他初始化是动态初始化。具有使用常量表达式(5.19)初始化的静态存储持续时间的POD类型(3.9)的对象应在任何动态初始化发生之前初始化。在同一翻译单元的命名空间范围内定义并动态初始化的静态存储持续时间的对象应按其定义在翻译单元中出现的顺序进行初始化。

如果您知道在初始化其他静态对象期间将使用此单例,我认为您会发现同步不是问题。据我所知,所有主要编译器都在一个线程中初始化静态对象,因此在静态初始化期间线程安全。您可以将单例指针声明为NULL,然后在使用之前检查它是否已初始化。

但是,这假设您知道您将在静态初始化期间使用此单例。标准也不保证这一点,因此如果您想要完全安全,请使用静态初始化的互斥锁。

编辑:克里斯建议使用原子比较和交换肯定会有效。如果可移植性不是问题(并且创建额外的临时单例不是问题),那么它是一个略低的开销解决方案。

答案 2 :(得分:10)

这是一个非常简单的懒惰构造的单例getter:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

这是懒惰的,下一个C ++标准(C ++ 0x)要求它是线程安全的。事实上,我相信至少g ++以线程安全的方式实现了这一点。那么,如果你的目标编译器,如果你使用的编译器也以线程安全的方式实现这一点(可能是较新的Visual Studio编译器吗?我不知道),那么这可能就是你需要的全部内容

另请参阅此主题的http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html

答案 3 :(得分:8)

如果没有任何静态变量,则无法执行此操作,但是如果您愿意容忍,则可以使用Boost.Thread来实现此目的。阅读“一次性初始化”部分以获取更多信息。

然后在你的单例访问器函数中,使用boost::call_once构造对象,然后返回它。

答案 4 :(得分:6)

对于gcc来说,这很容易:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

GCC将确保初始化是原子的。 对于VC ++,情况并非如此。 : - (

这种机制的一个主要问题是缺乏可测试性:如果你需要在测试之间将LazyType重置为新的,或者想要将LazyType *更改为MockLazyType *,那么你将无法做到。鉴于此,通常最好使用静态互斥+静态指针。

此外,可能还是一边:最好始终避免使用静态非POD类型。 (指向POD的指针是正常的。)原因很多:正如您所提到的,初始化顺序未定义 - 也不是调用析构函数的顺序。因此,程序在尝试退出时最终会崩溃;通常不是什么大不了的事,但是当你试图使用的探查器需要一个干净的退出时,有时候是一个showstopper。

答案 5 :(得分:1)

虽然这个问题已经得到解答,但我认为还有其他一些要点:

  • 如果您希望在使用指向动态分配实例的指针时对单例进行延迟实例化,则必须确保在正确的位置清理它。
  • 您可以使用Matt的解决方案,但您需要使用正确的互斥锁/关键部分进行锁定,并在锁定之前和之后检查“pObj == NULL”。当然, pObj 也必须是静态;) 。 在这种情况下,互斥锁会不必要地重,你最好选择一个关键部分。

但如前所述,如果不使用至少一个同步原语,则无法保证线程安全延迟初始化。

编辑:Yup Derek,你是对的。我的错。 :)

答案 6 :(得分:1)

  1. 阅读弱内存模型。它可以打破双重检查锁和自旋锁。英特尔是强大的内存型号(英特尔),因此英特尔更容易

  2. 小心使用“volatile”来避免在寄存器中缓存对象的部分,否则你将初始化对象指针,而不是对象本身,而另一个线程将崩溃

  3. 静态变量初始化与共享代码加载的顺序有时并非无足轻重。我已经看到破坏对象的代码已经卸载的情况,所以程序在退出时崩溃了

  4. 此类物品难以正确销毁

  5. 一般来说,单身人士很难做到正确且难以调试。最好完全避免它们。

答案 7 :(得分:1)

  

您可以使用Matt的解决方案,但您需要使用正确的互斥锁/关键部分进行锁定,并在锁定之前和之后检查“pObj == NULL”。当然,pObj也必须是静态的;)。在这种情况下,互斥量会不必要地重,你最好选择一个关键部分。

OJ,这不起作用。正如克里斯指出的那样,这是双重检查锁定,不能保证在当前的C ++标准中有效。请参阅:C++ and the Perils of Double-Checked Locking

编辑:没问题,OJ。它在确实有用的语言中非常好用。我希望它可以在C ++ 0x中工作(虽然我不确定),因为它是如此方便的习惯用法。

答案 8 :(得分:0)

我想说不要这样做是因为它不安全,并且可能会比在main()中初始化这些东西更频繁地破坏,并不会那么受欢迎。

(是的,我知道这意味着你不应该尝试在全局对象的构造函数中做有趣的事情。这就是重点。)