繁体   English   中英

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

[英]efficient thread-safe singleton in C++

单身类的通常模式就像

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?

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

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

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

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

这篇Meyers / Alexandrescu论文解释了为什么 - 但这篇论文也被广泛误解。 它开始了'双重检查锁定在C ++中的不安全'模因 - 但它的实际结论是C ++中的双重检查锁定可以安全地实现,它只需要在非显而易见的地方使用内存屏障。

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

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

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

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

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

您的替代方案称为“双重检查锁定”

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

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

你可以在这里找到源代码。

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

解决方案不是线程安全的,因为声明

inst = new Foo();

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

Statement1:inst = malloc(sizeof(Foo));
Statement2:inst-> Foo();

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM