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

86

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

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

然而,据我所知,该解决方案不是线程安全的,因为1)Foo的构造函数可能会被调用多次(这可能重要,也可能不重要),2)实例(inst)在返回给不同的线程之前可能尚未完全构造。

一种解决方法是在整个方法周围包装一个互斥锁(mutex),但这样做会导致我在实际需要之后长时间支付同步开销。另一种解决方法是类似于:

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吗?


6
但是你没有时间找一个例子并引导投票吗?目前我也没有任何材料可提供。 - bmargulies
1
可能是 https://dev59.com/TXVD5IYBdhLWcg3wWaRh 的重复问题。 - kennytm
3
@bmargulies 不,问问题的人显然不想费心,那我为什么要费心呢?我已经决定放弃点踩和关闭重复问题了,好像我是为了让 Stack Overflow 保持良好而费心的少数人之一。你知道吗,懒散感觉很好! - anon
我确实花时间仔细描述了我的问题,提供了代码片段并讨论了我所知道和尝试过的内容。很抱歉我浪费了您的时间。:( - user168715
1
@sbi:我也是这么认为。在成千上万的问题中分散答案是使之后难以搜索的最佳方式。 - Matthieu M.
显示剩余2条评论
9个回答

113

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

Foo& getInst()
{
    static Foo inst(...);
    return inst;
}
根据新标准,不再需要担心这个问题。对象初始化将仅由一个线程完成,其他线程将等待其完成。
或者您可以使用std::call_once。(更多信息请参见此处)

3
这是我期望人们实现的 C++11 解决方案。 - Alexander Oh
8
很遗憾,这在VS2013中不是线程安全的,请参见此处的“Magic Statics”:http://msdn.microsoft.com/en-gb/library/hh567368.aspx - Chris Drew
4
看起来 VS 14 已经解决了这个问题 -- http://blogs.msdn.com/b/vcblog/archive/2014/06/03/visual-studio-14-ctp.aspx - Sea Coast of Tibet
9
为避免混淆,也许你可以在函数声明中添加static修饰符,或显式声明该函数为非成员函数。 - MikeMB
这些实例的调用是否来自不同的线程是线程安全的,还是实例化类的函数必须自行处理原子性? - Shadasviar
使用 std::call_once:http://www.modernescpp.com/index.php/thread-safe-initialization-of-data,并查看 Scott Meyers 的 Singleton。 - Swapnil

49

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

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

该论文包含了演示如何使用内存屏障来安全实现DLCP的伪代码,所以你应该很容易就能纠正你的实现。


如果(inst == NULL) { temp = new Foo(...); inst=temp;} 这不是保证构造函数在分配inst之前已经完成了吗?我知道它可能会被优化掉,但从逻辑上讲,这不是解决了问题吗? - stu
1
这并没有帮助,因为符合规范的编译器可以自由地重新排列赋值和构造步骤。 - JoeG
我仔细阅读了这篇论文,似乎建议是避免在Singleton中使用DLCP。你需要对类进行大量的volatile操作,并添加内存屏障(这不会影响效率吗?)。为了实际需求,使用一个简单的单锁,并缓存从“GetInstance”获取的对象即可。 - Guy
我刚刚阅读了这篇论文,我理解的主要结论是:DLCP可以使用内存屏障实现线程安全,但在不可移植的方式下(在C++11之前)。 - 463035818_is_not_a_number

14

Herb Sutter在CppCon 2014中谈到了C++中的双重检查锁。

下面是我基于此实现的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
5
在实例函数内部,一个简单的 static Foo foo;return &foo; 就足够了;在 C++11 中,static 初始化是线程安全的。尽量使用引用(reference)而不是指针(pointers)。 - Etherealone
我在 MSVC 2015 中收到以下错误信息:Severity Code Description Project File Line Source Suppression State Error (active) more than one operator "==" matches these operands。 - user2286810
@qqibrow,也许你可以将复制构造函数、移动构造函数、赋值运算符和移动赋值运算符设置为私有。 - Mayur

11

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

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


3
据我所知,唯一保证线程安全的方法是在启动任何线程之前初始化所有单例对象。

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

0

你的替代方案被称为"双重检查锁定"

虽然可能存在多线程内存模型使其可行,但POSIX并不保证其中一个。


0

ACE 单例模式使用双重检查锁定模式来保证线程安全,如果您喜欢,可以参考它。

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


-2

该解决方案不具备线程安全性,因为语句

inst = new Foo();

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

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

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


1
不,这是不安全的,没有任何疑问。它不必被编译器“分解”才能变得不安全。 - curiousguy

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