C ++中高效的线程安全单例


76

单例课程的通常模式如下

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
}

但是,据我了解,此解决方案不是线程安全的,因为1)Foo的构造函数可能被多次调用(可能无关紧要),并且2)inst在返回到另一个线程之前可能未完全构建。

一种解决方案是在整个方法周围包裹一个互斥体,但是在我真正需要同步后很长一段时间内,我就要为同步开销付出代价。另一种方法是

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
  {
    pthread_mutex_lock(&mutex);
    if(inst == NULL)
      inst = new Foo(...);
    pthread_mutex_unlock(&mutex);
  }
  return *inst;    
}

这是正确的方法,还是我应该注意的陷阱?例如,是否可能发生任何静态初始化顺序问题,即inst总是在首次调用getInst时始终保证为NULL?


6
但是您没有时间查找示例并进行密切投票吗?我现在很新鲜。
bmargulies 2010年


3
@bmargulies不,发问者显然不会受到打扰,所以我为什么要这样做?我已经决定放弃以低调为底的全权委托,因为我似乎是为数不多的不愿接受SO的人之一。而且您知道,懒惰感觉很好!

我确实花了一些时间来仔细描述我的问题,包括摘要和对我知道/曾经尝试过的内容的讨论。对不起,我浪费了您的时间。:(
user168715 '04

1
@sbi:我也是。将答案分散在成千上万个问题中是最好的方法,以后很难搜索它们。
Matthieu M.

Answers:


44

您的解决方案称为“双重检查锁定”,并且您编写的方法也不是线程安全的。

迈尔斯/ Alexandrescu的论文解释了为什么-但该文件也被广泛误解。它开始了“双重检查锁定在C ++中是不安全的”模因-但它的实际结论是,可以安全地实现C ++中的双重检查锁定,只需要在不明显的地方使用内存屏障。

本文包含伪代码,该伪代码演示了如何使用内存屏障安全地实现DLCP,因此对您的实现进行纠正应该不难。


if(inst == NULL){temp =新的Foo(...); inst = temp;}不能保证构造函数在分配inst之前完成?我意识到可以(并且可能会)对其进行优化,但是从逻辑上讲可以解决问题,不是吗?
stu

1
这无济于事,因为兼容的编译器可以自由地重新排列分配和构造步骤的顺序。
JoeG

我仔细阅读了这篇文章,似乎建议不要使用Singleton来避免DLCP。您将不得不摆脱类的麻烦,并增加内存壁垒(也不会影响效率吗?)。出于实际需要,请使用简单的单锁,并缓存从“ GetInstance”获取的对象。
guyarad 2015年

也刚读了这篇文章,我了解到主要结论是:DLCP可以使用内存屏障来实现线程安全,不能以可移植的方式(在c ++ 11之前)实现
maximum_prime_is_463035818 17/11/17

101

如果您使用的是C ++ 11,这是执行此操作的正确方法:

Foo& getInst()
{
    static Foo inst(...);
    return inst;
}

根据新标准,不再需要关心这个问题。对象初始化将仅由一个线程进行,其他线程将等待其完成。或者,您可以使用std :: call_once。(更多信息在这里


2
这是我希望人们实现的C ++ 11解决方案。
亚历山大·吴

8
遗憾的是,这在VS2013中不是线程安全的,请参见此处的“ Magic Statics”:msdn.microsoft.com/en-gb/library/hh567368.aspx
Chris Drew


8
为避免混淆,您可以将static添加到函数声明中,也可以明确声明它是非成员函数。
MikeMB

来自不同线程的对此实例的调用是线程安全的,还是实例化类的函数必须自己照顾原子性?
Shadasviar '18年

12

Herb Sutter讨论了CppCon 2014中的双重检查锁定。

以下是基于此我在C ++ 11中实现的代码:

class Foo {
public:
    static Foo* Instance();
private:
    Foo() {}
    static atomic<Foo*> pinstance;
    static mutex m_;
};

atomic<Foo*> Foo::pinstance { nullptr };
std::mutex Foo::m_;

Foo* Foo::Instance() {
  if(pinstance == nullptr) {
    lock_guard<mutex> lock(m_);
    if(pinstance == nullptr) {
        pinstance = new Foo();
    }
  }
  return pinstance;
}

您还可以在此处检查完整的程序:http : //ideone.com/olvK13


1
@Etherealone您的建议是什么?
qqibrow

4
一个简单的static Foo foo;return &foo;实例函数内部就足够了; static在C ++ 11中,初始化是线程安全的。虽然更喜欢引用指针。
Etherealone

我在MSVC 2015中收到错误消息:严重性代码说明项目文件行源抑制状态错误(活动),多个运算符“ ==”匹配以下操作数:
user2286810 '16

@qqibrow可能是您也可以将复制构造函数,移动构造函数,赋值运算符,移动赋值运算符设为私有。
Mayur

9

采用 pthread_once,可以确保初始化函数自动运行一次。

(在Mac OS X上,它使用自旋锁。不知道其他平台的实现。)


3

TTBOMK,唯一无需执行锁定即可保证线程安全的方法是启动线程之前初始化所有单例。



0

ACE单例实现使用双重检查锁定模式来确保线程安全,您可以根据需要引用它。

您可以在此处找到源代码。



-2

该解决方案不是线程安全的,因为该语句

inst = new Foo();

可以由编译器分解为两个语句:

声明1:inst = malloc(sizeof(Foo));
声明2:inst-> Foo();

假设在由一个线程执行语句1之后发生上下文切换。并且第二线程也执行该getInstance()方法。然后,第二个线程将发现“ inst”指针不为空。因此,第二线程将返回指向未初始化对象的指针,因为第一线程尚未调用构造函数。


不,这是不安全的。不必为了安全起见就必须“将其分解”。
curiousguy
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.