首页 > 代码库 > C++中多线程与Singleton的那些事儿

C++中多线程与Singleton的那些事儿

前言

  前段时间在网上看到了一个百度的面试题,大概意思是如何在不使用锁和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++11 standard中的§6.7 [stmt.dcl] p4:

If control enters the declaration concurrently while the variable is being initialized, the concurrent 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 22         ~Helper()23         {24             delete value_;25             value_ = NULL;26         }27     };28 29     friend class Helper;30 31     static T*      value_;32     static Helper  helper_;33 };34 35 template<typename T>36 T* Singleton<T>::value_ = NULL;37 38 template<typename T>39 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对象。

参考资料

[1] 梅耶 (Scott Meyers). Effective C++. 电子工业出版社, 2011

[2] 斯坦利·B.李普曼. 深入探索C++对象模型. 电子工业出版社, 2012

[3] 陈良桥(译). C++11 FAQ中文版

[4] Bjarne Stroustrup. C++11 FAQ

[5] C++11 standard

[6] 史蒂文斯 (W.Richard Stevens). UNIX环境高级编程, 人民邮电出版社, 2014

[7] stackoverflow. Is Meyers implementation of Singleton pattern thread safe?

(完)

C++中多线程与Singleton的那些事儿