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

Posted

技术标签:

【中文标题】Meyers 实现的 Singleton 模式线程安全吗?【英文标题】:Is Meyers' implementation of the Singleton pattern thread safe? 【发布时间】:2010-12-12 07:16:46 【问题描述】:

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

static Singleton& instance()

     static Singleton s;
     return s;

如果不是,为什么以及如何使其线程安全?

【问题讨论】:

谁能解释一下为什么这不是线程安全的。链接中提到的文章讨论了使用替代实现的线程安全性(使用指针变量,即静态 Singleton *pInstance)。 见:***.com/questions/449436/… 见:***.com/questions/1008019/c-singleton-design-pattern/… Singleton instance declared as static variable of GetInstance method的可能重复 【参考方案1】:

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

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

GCC 和 VS 对该功能的支持(Dynamic Initialization and Destruction with Concurrency,也称为Magic Statics on MSDN)如下:

Visual Studio:自 Visual Studio 2015 起支持 GCC:自 GCC 4.3 起支持

感谢@Mankarse 和@olen_gam 的cmets。


在C++03 中,此代码不是线程安全的。 Meyers 有一篇名为 "C++ and the Perils of Double-Checked Locking" 的文章讨论了该模式的线程安全实现,结论或多或少是,(在 C++03 中)围绕实例化方法的完全锁定基本上是确保正确的最简单方法所有平台上的并发性,而大多数形式的双重检查锁定模式变体可能会受到race conditions on certain architectures 的影响,除非指令与战略性的内存屏障交错。

【讨论】:

Alexandrescu 在 Modern C++ Design 中也对单例模式(生命周期和线程安全)进行了广泛的讨论。查看 Loki 的网站:loki-lib.sourceforge.net/index.php?n=Pattern.Singleton 你可以使用 boost::call_once 创建一个线程安全的单例。 很遗憾,这部分标准没有在 Visual Studio 2012 C++ 编译器中实现。在此处的“C++11 核心语言特性:并发”表中称为“Magic Statics”:msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx 标准中的 sn-p 解决了构建而不是破坏。标准是否防止对象在一个线程上被破坏,而(或之前)另一个线程试图在程序终止时访问它? IANA(C++ language)L 但第 3.6.3 节 [basic.start.term] p2 建议在对象被销毁后尝试访问对象可能会遇到未定义的行为?跨度> 【参考方案2】:

要回答您关于为什么它不是线程安全的问题,这并不是因为第一次调用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)));

所以这是一个简单的线程安全单例(适用于 Windows)。它为 Windows CRITICAL_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;

伙计——“让全球变得更好”是一大堆废话。

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

如果new Singleton() 抛出,锁将不会被释放。这可以通过使用真正的 RAII 锁定对象而不是我这里的简单对象来解决。如果您使用 Boost 之类的工具为锁提供独立于平台的包装器,这也有助于使事情变得可移植。 这保证了在调用 main() 之后请求 Singleton 实例时的线程安全 - 如果您在此之前调用它(如在静态对象的初始化中),则可能无法正常工作,因为 CRITICAL_SECTION 可能未初始化。李> 每次请求实例时都必须锁定。正如我所说,这是一个简单的线程安全实现。如果你需要一个更好的(或者想知道为什么像双重检查锁定技术这样的东西是有缺陷的),请参阅papers linked to in Groo's answer。

【讨论】:

哦哦。如果new Singleton() 抛出,会发生什么? @Bob - 公平地说,有了一组适当的库,所有与不可复制性和适当的 RAII 锁有关的琐事都会消失或最小化。但我希望这个例子是合理的独立的。尽管单例的工作量可能很小,但我发现它们在管理全局变量的使用方面很有用。它们往往更容易找到它们的使用地点和时间,而不仅仅是命名约定。 @sbi:在这个例子中,如果new Singleton() 抛出,那么锁肯定有问题。应该使用适当的 RAII 锁定类,例如来自 Boost 的 lock_guard。我希望这个例子或多或少是独立的,它已经有点像怪物了,所以我放弃了异常安全(但把它叫出来了)。也许我应该解决这个问题,这样这段代码就不会被剪切粘贴到不合适的地方。 为什么要动态分配单例?为什么不直接将“pInstance”设为“Singleton::instance()”的静态成员? @Martin - 完成。你是对的,这让它变得更简单了——如果我使用 RAII 锁类会更好。【参考方案3】:

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

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

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

如果不是,为什么[...]?

原因是没有什么能阻止多个线程同时执行s' 构造函数。

如何使其线程安全?

"C++ and the Perils of Double-Checked Locking" 由 Scott Meyers 和 Andrei Alexandrescu 撰写,是一篇关于线程安全单例的非常好的论文。

【讨论】:

【参考方案6】:

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

【讨论】:

以上是关于Meyers 实现的 Singleton 模式线程安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

Android框架设计模式——Singleton Method

怎么实现一个线程安全的单例模式

设计模式Singleton

单例模式(Singleton Pattern)

一天一个设计模式——(Singleton)单例模式(线程安全性)

单例模式Singleton