Meyers 实施单例模式线程安全吗?

以下使用惰性初始模式实现的 Singleton(Meyers’Singleton)线程是否安全?

static Singleton& instance()
{
static Singleton s;
return s;
}

如果没有,为什么和如何使它线程安全?

64153 次浏览

正确答案取决于你的编译器。它可以决定 制造是线程安全的; 它不是“自然”线程安全的。

C + + 11中,它是线程安全的。根据 标准§6.7 [stmt.dcl] p4:

如果控制进入 在初始化变量时并发声明,完成初始化的 同时执行应等待

GCC 和 VS 对该特性(动态初始化和并发销毁,也称为 MSDN 上的神奇静力学)的支持如下:

感谢@Mankarse 和@olen _ gam 的评论。


C + + 03中,这段代码不是线程安全的。Meyers 有一篇名为 “ C + + 与双重检查锁定模式的危险”的文章讨论了模式的线程安全实现,其结论是(在 C + + 03中)围绕实例化方法的完全锁基本上是确保在所有平台上适当并发的最简单的方法,而大多数形式的双重检查锁定模式模式变体可能会受到 某些体系结构上的竞争条件的影响,除非指令与策略性地放置内存障碍交织在一起。

要回答为什么它不是线程安全的问题,并不是因为对 instance()的第一次调用必须调用 Singleton s的构造函数。为了线程安全,这必须发生在关键部分,但是标准中没有要求采用关键部分(迄今为止的标准在线程上是完全静默的)。编译器通常使用静态布尔值的简单检查和增量来实现这一点——但不是在关键部分。类似于下面的伪代码:

static Singleton& instance()
{
static bool initialized = false;
static char s[sizeof( Singleton)];


if (!initialized) {
initialized = true;


new( &s) Singleton(); // call placement new on s to construct it
}


return (*(reinterpret_cast<Singleton*>( &s)));
}

所以这里有一个简单的线程安全的 Singleton (针对 Windows)。它使用一个简单的类包装器来包装 Windows CRITICS _ SECTION 对象,这样我们就可以让编译器在调用 main()之前自动初始化 CRITICAL_SECTION。理想情况下,可以使用一个真正的 RAII 关键节类来处理在保持关键节时可能发生的异常,但这超出了本答案的范围。

基本操作是,当请求 Singleton的一个实例时,获取一个锁,如果需要,创建 Singleton,然后释放锁并返回 Singleton 引用。

#include <windows.h>


class CritSection : public CRITICAL_SECTION
{
public:
CritSection() {
InitializeCriticalSection( this);
}


~CritSection() {
DeleteCriticalSection( this);
}


private:
// disable copy and assignment of CritSection
CritSection( CritSection const&);
CritSection& operator=( CritSection const&);
};




class Singleton
{
public:
static Singleton& instance();


private:
// don't allow public construct/destruct
Singleton();
~Singleton();
// disable copy & assignment
Singleton( Singleton const&);
Singleton& operator=( Singleton const&);


static CritSection instance_lock;
};


CritSection Singleton::instance_lock; // definition for Singleton's lock
//  it's initialized before main() is called




Singleton::Singleton()
{
}




Singleton& Singleton::instance()
{
// check to see if we need to create the Singleton
EnterCriticalSection( &instance_lock);
static Singleton s;
LeaveCriticalSection( &instance_lock);


return s;
}

伙计,“让世界变得更美好”真是一派胡言。

这个实现的主要缺点(如果我没有漏掉一些 bug 的话)是:

  • 如果 new Singleton()抛出,锁就不会被释放。这个问题可以通过使用真正的 RAII 锁对象来解决,而不是我在这里使用的简单的 RAII 锁对象。如果您使用诸如 Boost 之类的工具为锁提供独立于平台的包装器,那么这也有助于实现可移植性。
  • 这保证了在调用 main()之后请求 Singleton 实例时的线程安全性——如果您在此之前调用它(就像在静态对象的初始化中一样) ,那么事情可能无法工作,因为 CRITICAL_SECTION可能没有被初始化。
  • 每次请求实例时都必须采用锁。如前所述,这是一个简单的线程安全实现。如果您需要更好的方法(或者想知道为什么像双重检查锁定技术这样的方法存在缺陷) ,请参阅 格鲁的回答中有关联的文件

下面的实现[ ... ]线程安全吗?

在大多数平台上,这不是线程安全的。(附加通常的免责声明,解释 C + + 标准不知道线程,因此,从法律上讲,它没有说它是否是。)

如果没有,为什么?

之所以不是这样,是因为没有什么可以阻止多个线程同时执行 s的构造函数。

如何使其线程安全?

“ c + + 和双重检查锁定模式的危险”(“ c + + 和 Andrei Alexandrescu 的危险”)是关于线程安全单例的一篇相当不错的论文。

正如 MSalters 所说: 这取决于您使用的 C + + 实现。查查文件。至于另一个问题: “如果没有,为什么?”C + + 标准还没有提到任何关于线程的内容。但是即将推出的 C + + 版本知道线程,并且它明确声明静态局部变量的初始化是线程安全的。如果两个线程调用这样一个函数,一个线程将执行初始化,而另一个线程将阻塞并等待它完成。

查看下一个标准(6.7.4节) ,它解释了静态本地初始化如何是线程安全的。因此,一旦这部分标准得到广泛实现,Meyer 的 Singleton 将成为首选实现。

我已经不同意很多答案了。大多数编译器已经以这种方式实现了静态初始化。一个值得注意的例外是 MicrosoftVisualStudio。