Meyers对Singleton模式线程的实现安全吗?


145

使用Singleton(Meyers'Singleton)线程的延迟初始化的以下实现安全吗?

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

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


有人可以解释为什么这不是线程安全的。链接中提到的文章讨论了使用替代实现(使用指针变量,即静态Singleton * pInstance)的线程安全性。
Ankur



Answers:


168

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

如果在初始化变量的同时控件同时输入了声明,则并发执行应等待初始化完成。

GCC和VS对功能(具有并发的动态初始化和销毁​​,在MSDN上也称为Magic Statics)的支持如下:

感谢@Mankarse和@olen_gam的评论。


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


3
Alexandrescu在Modern C ++ Design中还对Singleton模式(生命周期和线程安全性)进行了广泛的讨论。参见Loki的网站:loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.

1
您可以使用boost :: call_once创建一个线程安全的单例。
CashCow 2012年

1
不幸的是,标准的这一部分未在Visual Studio 2012 C ++编译器中实现。在此处的“ C ++ 11核心语言功能:并发性”表中称为“魔术静态”:msdn.microsoft.com/zh-cn/library/vstudio/hh567368.aspx
olen_garn

该标准的代码段仅涉及构造,但不涉及破坏。该标准是否防止对象在程序终止时(或之前)在另一个线程尝试访问它时在一个线程上被破坏?
stewbasic

IANA(C ++语言)L,但第3.6.3节[basic.start.term] p2建议是否可以通过在对象被销毁后尝试访问该对象来达到未定义的行为?
stewbasic

21

要回答有关为什么它不是线程安全的问题,不是因为第一次调用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 CRITICAL_SECTION对象使用了一个简单的类包装器,因此我们可以让编译器自动初始化CRITICAL_SECTIONbefore main()调用。理想情况下,将使用真正的RAII关键部分类,该类可以处理持有关键部分时可能发生的异常,但这超出了此答案的范围。

基本操作是,当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;
}

男人-“使世界变得更好”这是很多废话。

此实现的主要缺点(如果我不让一些错误溜走的话)是:

  • 如果new Singleton()抛出该锁,则不会释放该锁。可以通过使用真正的RAII锁定对象而不是在此使用的简单对象来解决此问题。如果您使用诸如Boost之类的工具为锁提供独立于平台的包装,这也可以使事情变得可移植。
  • 这样可确保在main()调用之后请求Singleton实例时确保线程安全-如果在此之前调用它(例如在静态对象的初始化中),则可能无法正常工作,因为CRITICAL_SECTION可能未初始化。
  • 每次请求实例时都必须采取锁定。如我所说,这是一个简单的线程安全实现。如果您需要更好的解决方案(或者想知道为什么像双重检查锁定技术这样的东西有缺陷),请参阅Groo的答案中链接到论文

1
哦哦 如果new Singleton()抛出会怎样?
09年

@Bob-公平的说,有了适当的库集,所有与不可复制性有关的碎片和适当的RAII锁都会消失或很小。但是我希望这个例子是合理的独立的。即使单例的工作量很大,也许获得的收益却很少,但我发现它们对于管理全局变量的使用很有用。与使用命名约定相比,它们倾向于使查找使用的位置和时间更容易一些。
Michael Burr,

@sbi:在此示例中,如果new Singleton()抛出异常,则锁肯定存在问题。应该使用适当的RAII锁类,例如lock_guardBoost中的类。我希望该示例或多或少是独立的,并且已经有点像怪物了,所以我放弃了异常安全性(但大声疾呼)。也许我应该解决此问题,以使此代码不会在不适当的地方被剪切粘贴。
Michael Burr,

为什么要动态分配单例?为什么不将'pInstance'设为'Singleton :: instance()'的静态成员呢?
马丁·约克

@马丁-完成。没错,这使它变得更简单-如果我使用RAII锁类,那就更好了。
迈克尔·伯

10

查看下一个标准(第6.7.4节),它说明了静态本地初始化如何保证线程安全。因此,一旦该标准的这一部分得到广泛实施,Meyer's Singleton将成为首选实施方案。

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


6

正确答案取决于您的编译器。它可以决定使其成为线程安全的。它不是“自然地”线程安全的。


5

下列实现线程安全吗?

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

如果没有,为什么[...]?

不是这样的原因是没有什么可以阻止一个以上的线程同时执行s'构造函数。

如何使其线程安全?

Scott Meyers和Andrei Alexandrescu 撰写的“ C ++和双重检查锁定的危险”是关于线程安全单例的相当不错的论文。


2

正如MSalters所说:这取决于您使用的C ++实现。检查文档。至于另一个问题:“如果不是,为什么?” -C ++标准尚未提及有关线程的任何内容。但是即将发布的C ++版本知道线程,它明确指出静态本地变量的初始化是线程安全的。如果两个线程调用该函数,则一个线程将执行初始化,而另一个线程将阻止并等待其完成。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.