很不幸,Matt的答案采用了所谓的“双重检查锁定”,而这种方法并不受C/C++内存模型的支持。(Java 1.5及更高版本和.NET内存模型支持该方法)。这意味着在执行
pObj == NULL
检查和获取锁(互斥体)之间,pObj
可能已经在另一个线程上被分配。线程切换发生在操作系统想要切换时,而不是程序的“行”(在大多数语言中编译后没有意义)之间。
此外,正如Matt所承认的那样,他使用一个int作为锁,而不是操作系统原语。不要这样做。适当的锁需要使用内存屏障指令、潜在的缓存行刷新等;使用操作系统的原语进行锁定。这尤其重要,因为所使用的原语可能会在操作系统运行的各个CPU线之间发生变化;在CPU Foo上有效的方法可能在CPU Foo2上无效。大多数操作系统都本地支持POSIX线程(pthreads),或者将它们作为操作系统线程包装器提供,所以最好使用它们来说明例子。
如果您的操作系统提供了适当的原语,并且如果您绝对需要它来提高性能,那么您可以使用原子比较和交换操作来初始化共享全局变量。基本上,您编写的内容将如下所示:
MySingleton *MySingleton::GetSingleton() {
if (pObj == NULL) {
MySingleton *temp = new MySingleton();
if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
delete temp;
}
}
return pObj;
}
只有在安全创建多个单例实例(每个线程同时调用GetSingleton()时创建一个)然后扔掉多余的实例时,此方法才有效。Mac OS X提供的OSAtomicCompareAndSwapPtrBarrier
函数(大多数操作系统都提供类似的原语)检查pObj
是否为NULL
,只有在它是NULL
时才将其设置为temp
。这使用硬件支持来确保只执行一次交换并告诉是否发生了交换。
如果您的操作系统提供了介于这两个极端之间的另一种方法,则可以利用pthread_once
。这使您能够设置仅运行一次的函数 - 基本上是通过为您进行所有锁定/障碍等技巧 - 无论它被调用多少次或在多少个线程上调用。