我知道线程安全单例的常见实现如下所示:
Singleton* Singleton::instance() {
if (pInstance == 0) {
Lock lock;
if (pInstance == 0) {
Singleton* temp = new Singleton; // initialize to temp
pInstance = temp; // assign temp to pInstance
}
}
return pInstance;
}
但为什么他们说这是一个线程安全的实现呢?
例如,第一个线程可以通过pInstance == 0
上的两个测试,创建new Singleton
并将其分配给temp
指针,然后开始赋值pInstance = temp
(据我所知,指针赋值操作不是原子的)
同时第二个线程测试第一个pInstance == 0
,其中pInstance
只分配了一半。它不是nullptr,但也不是有效的指针,然后从函数返回。
这样的情况会发生吗?我没有在任何地方找到答案,似乎这是一个非常正确的实现,我什么都不懂
答案 0 :(得分:4)
C ++并发规则并不安全,因为pInstance
的第一次读取不受锁或类似的保护,因此不能与写入正确同步( 受保护)。因此存在数据竞争,因此存在未定义的行为。这个UB的一个可能的结果正是你已经确定的:第一次检查读取了pInstance
的垃圾值,它只是由不同的线程写的。
常见的解释是,在更常见的情况下(Singleton& Singleton::instance() {
static Singleton s;
return s;
}
已经有效),它可以节省获取锁(可能是时间昂贵的操作)。但是,这不安全。
由于C ++ 11及更高版本保证了函数范围的初始化,静态变量只发生一次并且是线程安全的,因此在C ++中创建单例的最佳方法是在函数中使用静态局部函数:
pInstance
请注意,不需要动态分配或指针返回类型。
在评论中提到的Voo,上面假设std::atomic<Singleton*>
是一个原始指针。如果它是nth-child
,则代码可以正常工作。当然,这是一个问题,原子读取是否比获取锁定慢得多,这应该通过分析来回答。尽管如此,这将是一个相当无意义的练习,因为静态局部变量在所有但非常模糊的情况下都更好。