如何高效且线程安全地实现单例? [重复]

问题描述 投票:0回答:9

单例类的通常模式是这样的

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++ singleton thread-safety pthreads
9个回答
113
投票

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

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

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


49
投票

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

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

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


14
投票

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


11
投票

使用

pthread_once
,保证初始化函数原子地运行一次。

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


3
投票

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


0
投票
您的替代方案称为

“双重检查锁定”

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


0
投票
ACE单例实现使用双重检查锁定模式来保证线程安全,如果你愿意,可以参考。

您可以在

这里找到源代码。


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

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

inst = new Foo();

可以被编译器分解为两条语句:

语句1:inst = malloc(sizeof(Foo));

语句2:inst->Foo();

假设一个线程执行完语句1后,发生了上下文切换。第二个线程也执行

getInstance()

 方法。然后第二个线程会发现'inst'指针不为空。因此,第二个线程将返回指向未初始化对象的指针,因为第一个线程尚未调用构造函数。

© www.soinside.com 2019 - 2024. All rights reserved.