线程安全的 C++ Singleton 实现

来源:互联网 发布:网络延迟单位 编辑:程序博客网 时间:2024/04/26 16:38

原文地址:

http://www.cnblogs.com/liyuan989/p/4264889.html

前言

前段时间在网上看到了一个百度的面试题,大概意思是如何在不使用锁和C++11的情况下,用C++实现线程安全的Singleton。

看到这个题目后,第一个想法就是用Scott Meyer在《Effective C++》中提到的,把non-local static变量放到static成员函数中来实现,但是经过一番查找轮子,这种实现在某些情况下是有问题的。本文主要将从最基本的单线程中的Singleton开始,慢慢讲述多线程与Singleton的那些事。

单线程

在多线程下,下面这个是常见的写法:

 1 template<typename T> 2 class Singleton 3 { 4 public: 5     static T& getInstance() 6     { 7         if (!value_) 8         { 9             value_ = new T();10         }11         return *value_;12     }13 14 private:15     Singleton();16     ~Singleton();17 18     static T* value_;19 };20 21 template<typename T>22 T* Singleton<T>::value_ = NULL;

在单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了。

多线程加锁

在多线程的环境中,上面单线程的写法就会产生race condition从而产生多次初始化的情况。要想在多线程下工作,最容易想到的就是用锁来包含shared variable了。下面是伪代码:

 1 template<typename T> 2 class Singleton 3 { 4 public: 5     static T& getInstance() 6     { 7         { 8             MutexGuard guard(mutex_)  // RAII 9             if (!value_)10             {11                 value_ = new T();12             }13         }14         return *value_;15     }16 17 private:18     Singleton();19     ~Singleton();20 21     static T*     value_;22     static Mutex  mutex_;23 };24 25 template<typename T>26 T* Singleton<T>::value_ = NULL;27 28 template<typename T>29 Mutex Singleton<T>::mutex_;

这样在多线程下就能正常工作了。这时候,可能有人会站出来说这种做法每次调用getInstance的时候都会进入临界区,在频繁调用getInstance的时候会比较影响性能。这个时候,DCL写法出现了。

DCL

DCL即double-checked locking。在普通加锁的写法中,每次调用getInstance都会进入临界区,这样在heavy contention的情况下该函数就会成为系统性能的瓶颈,这个时候就有先驱者们想到了DCL写法,也就是进行两次check,当第一次check为假时,才加锁进行第二次check:

 1 template<typename T> 2 class Singleton 3 { 4 public: 5     static T& getInstance() 6     { 7         if(!value_) 8         { 9             MutexGuard guard(mutex_);10             if (!value_)11             {12                 value_ = new T();13             }14         }15         return *value_;16     }17 18 private:19     Singleton();20     ~Singleton();21 22     static T*     value_;23     static Mutex  mutex_;24 };25 26 template<typename T>27 T* Singleton<T>::value_ = NULL;28 29 template<typename T>30 Mutex Singleton<T>::mutex_;

是不是觉得这样就完美啦?其实在一段时间内,大家都以为这种做法正确的、有效的做法。幸运的是,后来有大牛们发现了DCL中的问题,避免了这样错误的写法在更多的程序中出现。

那么到底错在哪呢?我们先看看第12行value_ = new T这一句发生了什么:

  1. 分配了一个T类型对象所需要的内存。
  2. 在分配的内存出构造T类型的对象。
  3. 把分配的内存的地址赋给指针value_

主观上,我们会觉得计算机在会按照123的步骤来执行代码,但是问题就出在这。实际上只能确定步骤1最先执行,而步骤2、3的执行顺序却是不一定的。假如某一个线程A在调用getInstance的时候第12行的语句按照132的步骤执行,那么当刚刚执行完步骤3的时候发生线程切换,计算机开始执行另外一个线程B。因为第一次check没有锁保护,那么在线程B中调用getInstance的时候,不会在第一此check上等待,而是执行这一句,那么此时value_已经被赋值了,就会直接返回该值然后执行后面使用T对象的语句,但是在A线程中步骤3还没有执行!也就是说在B线程中通过getInstance返回的对象还没有被构造就被拿去使用了!这样就会发生一些难以debug的灾难。

volatile关键字也不会影响执行顺序的不确定性。

在多核心机器的环境下,2个核心同时执行上面的A、B两个线程时,由于第一次check没有锁保护,依然会出现使用实际没有被构造的对象这些情况。

关于DCL问题的详细介绍,可以参考Scott Meyer的paper: 《C++ and the Perils of Double-Checked Locking》

不过在新的C++11中,这个问题得到了解决。因为新的C++11规定了新的内存模型,保证了上述的执行顺序是123,DCL又可以正确使用了,不过在C++11下却有更简洁的多线程Singleton写法了,这个留在后面再介绍。

关于新的C++11的内存模型,可以参考: C++11中文版FAQ:内存模型 、C++11FAQ:Memory Model 、 C++ Data-Dependency Ordering: Atomics and Memory Model

Meyers Singleton

Scott Meyer在《Effective C++》中提出了一种简洁的singleton写法

 1 template<typename T> 2 class Singleton 3 { 4 public: 5     static T& getInstance() 6     { 7         static T value; 8         return value; 9     }10 11 private:12     Singleton();13     ~Singleton();14 };

先说结论:

  • 单线程下,正确。
  • C++11及以后的版本(如C++14)的多线程下,正确。
  • C++11之前的多线程下,不一定正确。

原因在于在C++11之前的标准中并没有规定local static变量的内存模型,所以很多编译器在实现local static变量的时候仅仅是进行了一次check(参考《深入探索C++对象模型》),于是getInstance函数被编译器改写成这样了:

 1 bool initialized = false; 2 char value[sizeof(T)]; 3  4 T& getInstance() 5 { 6     if (!initialized) 7     { 8        initialized = true; 9        new (value) T();10     }11     return *(reinterpret_cast<T*>(value));12 }

于是乎它就是不是线程安全的了。

但是在C++11却是线程安全的,这是新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须等到该初始化完成以后才能访问它。

在 C++ standard 中的§6.7 [stmt.dcl] p4:

If control enters the declaration concurrently while the variable is being initialized, theconcurrent execution shall wait for completion of the initialization.

在stackoverflow中的 Is Meyers implementation of Singleton pattern thread safe? 这个问题中也有讨论到。

不过有些编译器在C++11之前的版本就支持这种模型,例如g++,从g++4.0开始,meyers singleton就是线程安全的,不需要C++11。其他的编译器需要具体的去查相关的官方手册了。

Atomic Singleton

在C++11之前的版本下,除了通过锁实现线程安全的Singleton外,还可以利用各个编译器内置的atomic operation来实现。(假设类Atomic是封装的编译器提供的atomic operation)

 1 template<typename T> 2 class Singleton 3 { 4 public: 5     static T& getInstance() 6     { 7         while (true) 8         { 9             if (ready_.get())10             {11                 return *value_;12             }13             else14             {15                 if (initializing_.getAndSet(true))16                 {17                     // another thread is initializing, waiting in circulation18                 }19                 else20                 {21                     value_ = new T();22                     ready_.set(true);23                     return *value_;24                 }25             }26         }27     }28 29 private:30     Singleton();31     ~Singleton();32 33     static Atomic<bool>  ready_;34     static Atomic<bool>  initializing_;35     static T*            value_;36 };37 38 template<typename T>39 Atomic<int> Singleton<T>::ready_(false);40 41 template<typename T>42 Atomic<int> Singleton<T>::initializing_(false);43 44 template<typename T>45 T* Singleton<T>::value_ = NULL;

肯定还有其他的写法,但是思路都是要区分三种状态:

  • 对象已经构造完成
  • 对象还没有构造完成,但是某一线程正在构造中
  • 对象还没有构造完成,也没有任何线程正在构造中

pthread_once

如果是在unix平台的话,除了使用atomic operation外,在不适用C++11的情况下,还可以通过pthread_once来实现Singleton。

pthread_once的原型为

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))

APUE中对于pthread_once是这样说的:

如果每个线程都调用pthread_once, 系统就能保证初始化话例程init_routine只被调用一次 ,即在系统首次调用pthread_once时。

所以,我就可以这样来实现Singleton了

 1 template<typename T> 2 class Singleton : Nocopyable 3 { 4 public: 5     static T& getInstance() 6     { 7         threads::pthread_once(&once_control_, init); 8         return *value_; 9     }10 11 private:12     static void init()13     {14         value_ = new T();15     }16 17     Singleton();18     ~Singleton();19 20     static pthread_once_t  once_control_;21     static T*              value_;22 };23 24 template<typename T>25 pthread_once_t Singleton<T>::once_control_ = PTHREAD_ONCE_INIT;26 27 template<typename T>28 T* Singleton<T>::value_ = NULL;

如果我们需要正确的释放资源的话,可以在init函数里面通过glibc提供的atexit函数来注册释放函数,从而达到了只在进程退出时才释放资源的这一目的。

static object

现在再回头看看本文开头说的面试题的要求,不用锁和C++11,那么可以通过atomic operation来实现,但是有人会说atomic不是夸平台的,各个编译器的实现不一样。那么其实通过static object来实现也是可行的。

 1 template<typename T> 2 class Singleton 3 { 4 public: 5     static T& getInstance() 6     { 7         return *value_; 8     } 9 10 private:11     Singleton();12     ~Singleton();13 14     class Helper15     {16     public:17         Helper()18         {19             Singleton<T>::value_ = new T();20         }21         ~Helper()22         {23             delete value_;24             value_ = NULL;25         }26     };27 28     friend class Helper;29 30     static T*      value_;31     static Helper  helper_;32 };33 34 template<typename T>35 T* Singleton<T>::value_ = NULL;36 37 template<typename T>38 typename Singleton<T>::Helper Singleton<T>::helper_;

这种写法有一个前提就是不能在main函数执行之前调用getInstance,因为C++标准只保证静态变量在main函数之前之前被构造完成。

local static

上面一种写法只能在进入main函数后才能调用getInstance,那么有人说,我要在main函数之前调用怎么办?

嗯,办法还是有的。这个时候我们就可以利用local static来实现,C++标准包装函数内的local static变量在函数调用之前被初始化构造完成,利用这一特性我们可以这样来做

 1 template<typename T> 2 class Singleton 3 { 4 private: 5     Singleton(); 6     ~Singleton(); 7  8     class Creater 9     {10     public:11         Creater()12             : value_(new T())13         {14         }15 16         ~Creater()17         {18             delete value_;19             value_ = NULL;20         }21 22         T& getValue()23         {24             return *value_;25         }26 27         T* value_;28     };29 30 public:31     static T& getInstance()32     {33         static Creater creater;34         return creater.getValue();35     }36 37 private:38     class Dummy39     {40     public:41         Dummy()42         {43             Singleton<T>::getInstance();44         }45     };46 47     static Dummy dummy_;48 };49 50 template<typename T>51 typename Singleton<T>::Dummy Singleton<T>::dummy_;

这样就可以了。dummy_作用即时在main函数之前没有调用getInstance,它依然会作为最后一道屏障保证在进入main函数之前构造完成Singleton对象。



0 0