有没有一种方法可以在C ++中实现单例对象:
以线程安全的方式懒散地构造(两个线程可能同时是单例的第一个用户-仍应只构造一次)。
不依赖于预先构造的静态变量(因此,单例对象本身在构造静态变量时可以安全使用)。
(我不太了解我的C ++,但是是在执行任何代码之前初始化整数和常量静态变量的情况(即,甚至在执行静态构造函数之前-它们的值可能已经在程序中"初始化"了)如果是的话-也许可以利用它来实现单例互斥体-进而可以用来保护实际单例的创建。)
太好了,看来我现在有几个不错的答案(可惜我不能将2或3标记为答案)。似乎有两种广泛的解决方案:
使用POD静态变量的静态初始化(而不是动态初始化),并使用内置原子指令实现自己的互斥量。这是我在问题中暗示的解决方案类型,我相信我已经知道。
使用其他一些库函数,例如pthread_once或boost :: call_once。这些我当然不知道-非常感谢发布的答案。
不幸的是,Matt的答案具有所谓的双重检查锁定,而C / C ++内存模型不支持该功能。 (它受Java 1.5及更高版本(我认为是.NET)的内存模型的支持。)这意味着在发生pObj == NULL检查和获取锁(互斥体)之间,pObj可能具有已经在另一个线程上分配。只要操作系统需要,线程切换就会发生,而不是在程序的"行"之间(在大多数语言中,它们在编译后都没有意义)发生。
此外,正如Matt所承认的那样,他使用int作为锁而不是OS原语。不要那样做正确的锁定要求使用内存屏障指令,可能的高速缓存行刷新等。使用操作系统的原语进行锁定。这一点特别重要,因为所使用的原语可能会在您的操作系统所运行的各个CPU线路之间发生变化。在CPU Foo上起作用的内容可能在CPU Foo2上不起作用。大多数操作系统本身都支持POSIX线程(pthread)或将它们作为OS线程包的包装提供,因此通常最好说明使用它们的示例。
如果您的操作系统提供了适当的原语,并且您绝对需要它来提高性能,则可以使用原子比较和交换操作来初始化共享的全局变量,而不是执行这种类型的锁定/初始化。本质上,您编写的内容将如下所示:
1 2 3 4 5 6 7 8 9 10 11 12
| 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;
} |
仅当可以安全地创建单个实例的多个实例(每个线程同时调用GetSingleton()的一个实例)安全然后丢弃其他对象时,此方法才有效。 Mac OS X上提供的OSAtomicCompareAndSwapPtrBarrier函数(大多数操作系统都提供了类似的原语),检查pObj是否为NULL,如果确实是,则仅将其设置为temp。这使用硬件支持,实际上仅执行一次交换并告诉它是否发生。
如果您的OS提供了介于这两个极端之间的另一个工具,则是pthread_once。这使您可以设置仅运行一次的功能-基本上是通过执行所有锁定/屏障/等等操作。麻烦的事-无论它被调用了多少次,或者被调用了多少个线程。
基本上,您要求同步创建一个单例,而不使用任何同步(先前构造的变量)。通常,这是不可能的。您需要一些可用于同步的东西。
关于您的其他问题,是的,可以静态初始化(即无需运行时代码)的静态变量保证在执行其他代码之前进行初始化。这样就可以使用静态初始化的互斥锁来同步单例的创建。
从2003年C ++标准修订版开始:
Objects with static storage duration (3.7.1) shall be zero-initialized (8.5) before any other initialization takes place. Zero-initialization and initialization with a constant expression are collectively called static initialization; all other initialization is dynamic initialization. Objects of POD types (3.9) with static storage duration initialized with constant expressions (5.19) shall be initialized before any dynamic initialization takes place. Objects with static storage duration defined in namespace scope in the same translation unit and dynamically initialized shall be initialized in the order in which their definition appears in the translation unit.
如果您知道在初始化其他静态对象时将使用此单例,那么我认为您会发现同步不是问题。据我所知,所有主要的编译器都在单个线程中初始化静态对象,因此在静态初始化期间具有线程安全性。您可以将单例指针声明为NULL,然后在使用它之前检查它是否已初始化。
但是,这假设您知道在静态初始化期间将使用此单例。标准也不能保证这一点,因此,如果要完全安全,请使用静态初始化的互斥锁。
编辑:克里斯的建议使用原子比较和交换肯定会奏效。如果可移植性不是问题(并且创建其他临时单例也不是问题),那么它是开销稍低的解决方案。
这是一个非常简单的惰性构造的单例getter:
1 2 3 4
| 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关于此主题。
没有任何静态变量就无法做到这一点,但是,如果您愿意容忍一个,则可以使用Boost.Thread来实现此目的。阅读"一次性初始化"部分以了解更多信息。
然后在您的单例访问器函数中,使用boost::call_once构造对象并返回它。
对于gcc,这非常简单:
1 2 3 4
| LazyType* GetMyLazyGlobal() {
static const LazyType* instance = new LazyType();
return instance;
} |
GCC将确保初始化是原子的。对于VC ++,情况并非如此。 :-(
这种机制的一个主要问题是缺乏可测试性:如果您需要在两次测试之间将LazyType重置为新的,或者想要将LazyType *更改为MockLazyType *,则将无法进行测试。鉴于此,通常最好使用静态互斥体+静态指针。
另外,可能还有一个缺点:最好始终避免使用静态非POD类型。 (指向POD的指针是可以的。)这样做的原因有很多:正如您提到的,初始化顺序未定义-析构函数的调用顺序也未定义。因此,当程序尝试退出时,它们最终将崩溃。通常没什么大不了的,但是当您尝试使用的探查器时,有时需要关闭窗口。
读取弱内存模型。它可能会破坏双重检查的锁和自旋锁。英特尔是强大的内存模型(还可以),因此在英特尔上它更容易
谨慎使用" volatile",以避免将对象的部分缓存到寄存器中,否则,您将初始化对象指针,而不是对象本身,并且其他线程将崩溃
静态变量初始化与共享代码加载的顺序有时并不容易。我已经看到了用于销毁对象的代码已经卸载的情况,因此程序在退出时崩溃
这样的东西很难销毁
通常,单例很难正确地进行调试。最好完全避免使用它们。
You could use Matt's solution, but you'd need to use a proper mutex/critical section for locking, and by checking"pObj == NULL" both before and after the lock. Of course, pObj would also have to be static ;) . A mutex would be unnecessarily heavy in this case, you'd be better going with a critical section.
OJ,那是行不通的。克里斯指出,这是双重检查锁定,不能保证在当前的C ++标准中可以使用。请参阅:C ++和双重检查锁定的风险
编辑:没问题,OJ。在可以使用的语言中,这确实很棒。我希望它可以在C ++ 0x中运行(尽管我不确定),因为它是一个方便的习惯用法。
尽管已经回答了这个问题,但我认为还有其他几点要提到:
-
如果要在使用指向动态分配的实例的指针时对单例进行延迟实例化,则必须确保在正确的位置清理它。
-
您可以使用Matt的解决方案,但需要使用适当的互斥/关键部分进行锁定,并在锁定之前和之后都检查" pObj == NULL"。当然,pObj也必须是静态的;)
。
在这种情况下,互斥体会不必要地变重,最好选择关键部分。
但是,如上所述,如果不使用至少一个同步原语,就无法保证线程安全的延迟初始化。
编辑:是的德里克,你是对的。我的错。 :)
我想说不要这样做,因为这样做不安全,而且可能会比仅仅在main()中初始化这些东西更容易破坏。
(是的,我知道这暗示着您不应尝试在全局对象的构造函数中做一些有趣的事情。这就是重点。)