梅耶斯实现的单例模式是否线程安全?

185

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

static Singleton& instance()
{
     static Singleton s;
     return s;
}
如果不安全,为什么不安全?如何使其线程安全?

有人能解释一下为什么这不是线程安全的吗?链接中提到的文章讨论了使用另一种实现方式(使用指针变量即静态Singleton *pInstance)来保证线程安全。 - Ankur
1
请参见:https://dev59.com/AXRB5IYBdhLWcg3w-8A9#449823 - Martin York
请参见:https://dev59.com/Q3NA5IYBdhLWcg3wVcJx#1008289 - Martin York
可能是GetInstance方法的静态变量声明为Singleton实例的重复问题。 - Trevor Boyd Smith
6个回答

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

如果在变量初始化时同时进入声明,则并发执行必须等待初始化完成。

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

感谢@Mankarse和@olen_gam的评论。


C++03中,此代码不是线程安全的。有一篇由Meyers撰写的文章,名为"C++和双重检查锁定的危险",讨论了该模式的线程安全实现,结论是,在所有平台上保证正确并发性的最简单方法(在C++03中)是完全锁定实例化方法,而大多数形式的双重检查锁定模式变体可能会在某些架构上出现竞争条件,除非指令与策略位置的内存屏障交错。

3
在《Modern C++ Design》一书中,Alexandrescu对Singleton模式(生命周期和线程安全性)进行了广泛讨论。请参阅Loki网站:http://loki-lib.sourceforge.net/index.php?n=Pattern.Singleton - Matthieu M.
1
你可以使用boost::call_once创建一个线程安全的单例。 - CashCow
1
不幸的是,Visual Studio 2012 C++编译器中没有实现标准的这部分内容。在此处称为“C++11核心语言特性:并发”表格中被称为“Magic Statics”:http://msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx。 - olen_garn
1
标准中的代码片段涉及构造而非析构。标准是否防止在程序终止时一个线程尝试访问对象之前或同时销毁该对象? - stewbasic
IANA(C++语言)L,但第3.6.3节[basic.start.term] p2表明,在尝试访问已被销毁的对象后,可能会发生未定义行为。 - stewbasic
可能是@MartinYork的答案的重复,针对相同的问题...但是早些时候提出 - Trevor Boyd Smith

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)));
}

这是一个简单的线程安全单例模式(适用于Windows)。它使用一个简单的类包装器来封装Windows的CRITICAL_SECTION对象,以便我们可以在调用main()之前让编译器自动初始化CRITICAL_SECTION。理想情况下,应该使用真正的RAII临界区类,以处理持有临界区时可能发生的异常,但这超出了本答案的范围。
基本操作是当请求Singleton实例时,会获取锁定,如果需要创建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可能未初始化。
  • 每次请求实例时都必须获取锁。正如我所说,这是一个简单的线程安全实现。如果您需要更好的实现(或想知道为什么像双重检查锁定技术这样的东西存在缺陷),请参见Groo答案中链接的论文

2
哦哦。如果new Singleton()抛出异常会发生什么? - sbi
@Bob - 公平地说,使用适当的库,所有与非可复制性和适当的RAII锁有关的琐事都会消失或最小化。但我希望这个示例是相对独立的。尽管单例可能需要很多工作,但我发现它们在管理全局变量的使用方面非常有用。它们往往比仅使用命名约定更容易找出它们何时何地被使用。 - Michael Burr
@Martin - 完成了。你是对的,这使得它变得简单了一些 - 如果我使用RAII锁类会更好。 - Michael Burr
通常情况下,如果可能存在竞争条件,我会使用boost::call_once来创建线程安全的单例。实际上,您可以创建一个可重复使用的模板模式来完成所有这些操作,这使得创建单例更加容易。(天哪!这意味着代码中将到处都是单例!!!)call_once内部的逻辑不能抛出异常,因此您的构造函数也不能这样做。 - CashCow
InitializeCriticalSection() 应该放在哪里,用于 instance_lock - user3819404
显示剩余2条评论

10

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

我对许多答案持不同意见。大多数编译器已经以这种方式实现了静态初始化。唯一突出的例外是Microsoft Visual Studio。


6

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


5
以下实现是否线程安全?
在大多数平台上,这并不是线程安全的。(添加通常的免责声明,解释C++标准不知道线程,因此在法律上它不会说明它是否线程安全。)
如果不是,为什么不是?
它之所以不是线程安全的,是因为没有任何东西阻止多个线程同时执行`s`的构造函数。
如何使其线程安全?
Scott Meyers和Andrei Alexandrescu的“C++和双重检查锁定的危险”是关于线程安全单例的一篇相当不错的论文。

2

正如MSalters所说:这取决于你使用的C++实现。请查看文档。至于另一个问题:“如果不是,为什么?”-- C++标准尚未涉及线程。但即将推出的C++版本知道线程,并明确指出静态本地变量的初始化是线程安全的。如果两个线程调用这样的函数,一个线程将执行初始化,而另一个线程将阻塞并等待其完成。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接