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

时间:2010-04-04 21:53:33

标签: c++ singleton thread-safety pthreads

单例类的常用模式类似于

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

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

9 个答案:

答案 0 :(得分:81)

如果您使用的是C ++ 11,这是一种正确的方法:

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

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

答案 1 :(得分:41)

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

Meyers/Alexandrescu paper解释了原因 - 但该论文也被广泛误解。它启动了“双重检查锁定在C ++中不安全”的模式 - 但它的实际结论是,C ++中的双重检查锁定可以安全地实现,它只需要在非显而易见的地方使用内存屏障。

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

答案 2 :(得分:11)

Herb Sutter talks about the double-checked locking in 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

答案 3 :(得分:8)

使用pthread_once,保证初始化函数以原子方式运行。

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

答案 4 :(得分:2)

TTBOMK,唯一保证线程安全的方法是在没有锁定的情况下执行此操作,而是在启动线程之前初始化所有单身

答案 5 :(得分:0)

您的替代方案称为"double-checked locking"

可能存在多线程内存模型,但是POSIX不能保证一个

答案 6 :(得分:0)

ACE单例实现使用双重检查锁定模式来确保线程安全,如果您愿意,可以参考它。

您可以找到源代码here

答案 7 :(得分:0)

TLS在这里工作吗? https://en.wikipedia.org/wiki/Thread-local_storage#C_and_C++

例如,

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

但我们还需要一种明确删除它的方法,比如

static void deleteInst() {
   if (!inst) {
     return;
   }
   delete inst;
   inst = NULL;
}

答案 8 :(得分:-1)

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

inst = new Foo();

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

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

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