前言
前段时间在网上看到了一个百度的面试题,大概意思是如何在不使用锁和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这一句发生了什么:
- 分配了一个T类型对象所需要的内存。
- 在分配的内存出构造T类型的对象。
- 把分配的内存的地址赋给指针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 else 14 { 15 if (initializing_.getAndSet(true)) 16 { 17 // another thread is initializing, waiting in circulation 18 } 19 else 20 { 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 Helper 15 { 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 Dummy 39 { 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?
(完)