4.解决多线程问题
上一篇实现的Singleton只能在单线程环境中使用,在多线程环境中会出现很多问题,看Instance()实现代码:
1 static Singleton& Instance() {
2 if (0 == _instance) { //1
3 _instance = new Singleton(); //2
4 atexit(Destroy);
5 }
6 return *_instance; //3
7 }
考虑如下情况:线程一调用Instance(),进入//1,0 == _instance 返回true,线程一于是进入//2。这时候线程一被挂起,线程二开始执行,线程二调用Instance(),进入//1,发现0 == _instance 仍然返回true,线程二于是也进入//2,线程二继续执行到//3直到返回。这时候线程一被唤醒,继续从//2开始执行,这将会覆盖线程二创建的_instance,线程一继续执行到//3直到返回...
解决方法很简单,引入相关同步对象(synchronization object)就行了,例如在win32平台下可以如下实现:
synobj.h
1 #ifndef SYNOBJ_H
2 #define SYNOBJ_H
3
4 #include <windows.h>
5
6 #define CLASS_UNCOPYABLE(classname) \
7 private: \
8 classname(const classname&); \
9 classname& operator=(const classname&);
10
11 class Mutex {
12 CLASS_UNCOPYABLE(Mutex)
13 public:
14 Mutex() :_cs() { InitializeCriticalSection(&_cs); }
15 ~Mutex() { DeleteCriticalSection(&_cs); }
16 void lock() { EnterCriticalSection(&_cs); }
17 void unlock() { LeaveCriticalSection(&_cs); }
18 private:
19 CRITICAL_SECTION _cs;
20 };
21
22 class Lock {
23 CLASS_UNCOPYABLE(Lock)
24 public:
25 explicit Lock(Mutex& cs) :_cs(cs) { _cs.lock(); }
26 ~Lock() { _cs.unlock(); }
27 private:
28 Mutex& _cs;
29 };
30
31 #endif/*SYNOBJ_H*/
有了同步对象很容易就能够写出如下代码:
singleton.h
1 #ifndef SINGLETON_H
2 #define SINGLETON_H
3
4 #include "synobj.h"
5
6 class Singleton {
7 public:
8 static Singleton& Instance() { // Unique point of access
9 Lock lock(_mutex);
10 if (0 == _instance) {
11 _instance = new Singleton();
12 atexit(Destroy); // Register Destroy function
13 }
14 return *_instance;
15 }
16 void DoSomething(){}
17 private:
18 static void Destroy() { // Destroy the only instance
19 if ( _instance != 0 ) {
20 delete _instance;
21 _instance = 0;
22 }
23 }
24 Singleton(){} // Prevent clients from creating a new Singleton
25 ~Singleton(){} // Prevent clients from deleting a Singleton
26 Singleton(const Singleton&); // Prevent clients from copying a Singleton
27 Singleton& operator=(const Singleton&);
28 private:
29 static Mutex _mutex;
30 static Singleton *_instance; // The one and only instance
31 };
32
33 #endif/*SINGLETON_H*/
singleton.cpp
1 #include "singleton.h"
2
3 Mutex Singleton::_mutex;
4 Singleton* Singleton::_instance = 0;
现在的Singleton虽然多线程安全,性能却受到了影响。从Instance()中可以看到,实际上仅仅当0 == _instance为true时才需要Lock。你很容易就写出如下代码:
1 static Singleton& Instance() {
2 if (0 == _instance) {
3 Lock lock(_mutex);
4 _instance = new Singleton();
5 atexit(Destroy);
6 }
7 return *_instance;
8 }
但是这样还是会产生竞争条件(race condition),一种广为人知的做法是使用所谓的Double-Checked Locking:
1 static Singleton& Instance() {
2 if (0 == _instance) {
3 Lock lock(_mutex);
4 if (0 == _instance) {
5 _instance = new Singleton();
6 atexit(Destroy);
7 }
8 }
9 return *_instance;
10 }
Double-Checked Locking机制看起来像是一个完美的解决方案,但是在某些条件下仍然不行。简单的说,编译器为了效率可能会重排指令的执行顺序(
compiler-based reorderings)。看这一行代码:
_instance = new Singleton();
在编译器未优化的情况下顺序如下:
1.new operator分配适当的内存;
2.在分配的内存上构造Singleton对象;
3.内存地址赋值给_instance。
但是当编译器优化后执行顺序可能如下:
1.new operator分配适当的内存;
2.内存地址赋值给_instance;
3.在分配的内存上构造Singleton对象。
当编译器优化后,如果线程一执行到2后被挂起。线程二开始执行并发现0 == _instance为false,于是直接return,而这时Singleton对象可能还未构造完成,后果...
上面说的还只是单处理器的情况,在多处理器(multiprocessors)的情况下,超线程技术必然会混合执行指令,指令的执行顺序更无法保障。关于Double-Checked Locking的更详细的文章,请看:
The "Double-Checked Locking is Broken" Declaration
5.使用volatile关键字
为了说明问题,请先考虑如下代码:
1 class MyThread : public Thread {
2 public:
3 virtual void run() {
4 while (!_stopped) {
5 //do something
6 }
7 }
8 void stop() {
9 _stopped = true;
10 }
11 private:
12 bool _stopped;
13 };
14
15 ...
16
17 MyThread thread;
18 thread.start();
上面用thread.start()开启了一个线程,该线程在while循环中检测bool标记_stopped,看是否该继续执行。如果想要结束这个线程,调用thread.stop()应该没问题。但是需要注意的是编译器很有可能对_stopped的存取进行优化。如果编译器发现_stopped被频繁存取(_stopped在while循环中),编译器可能会考虑将_stopped缓存到寄存器中,以后_stopped将会直接从寄存器存取。这时候如果某个线程调用了thread.stop(),对_stopped的修改将不会反映到寄存器中,thread将会永远循环下去...
为了防止编译器优化,用volatile关键字就OK了,volatile跟const的用法几乎一样,能用const的地方也都能用volatile。对Singleton来说,修改如下两处即可:
1 //singleton.h中
2 static Singleton *_instance;
3 //改为
4 static Singleton * volatile _instance;
5
6 //singleton.cpp中
7 Singleton* Singleton::_instance = 0;
8 //改为
9 Singleton* volatile Singleton::_instance = 0;
6.将Singleton泛化为模板
singleton.h
1 #ifndef SINGLETON_H
2 #define SINGLETON_H
3
4 #include "synobj.h"
5
6 template<class T>
7 class Singleton {
8 CLASS_UNCOPYABLE(Singleton)
9 public:
10 static T& Instance() { // Unique point of access
11 if (0 == _instance) {
12 Lock lock(_mutex);
13 if (0 == _instance) {
14 _instance = new T();
15 atexit(Destroy);
16 }
17 }
18 return *_instance;
19 }
20 protected:
21 Singleton(){}
22 ~Singleton(){}
23 private:
24 static void Destroy() { // Destroy the only instance
25 if ( _instance != 0 ) {
26 delete _instance;
27 _instance = 0;
28 }
29 }
30 static Mutex _mutex;
31 static T * volatile _instance; // The one and only instance
32 };
33
34 template<class T>
35 Mutex Singleton<T>::_mutex;
36
37 template<class T>
38 T * volatile Singleton<T>::_instance = 0;
39
40 #endif/*SINGLETON_H*/
测试代码:
test.cpp
1 #include "singleton.h"
2
3 class A : public Singleton<A> {
4 friend class Singleton<A>;
5 protected:
6 A(){}
7 ~A(){}
8 public:
9 void DoSomething(){}
10 };
11
12 int main() {
13
14 A &a = A::Instance();
15 a.DoSomething();
16
17 return 0;
18 }
7.Singleton的析构问题
到此Singleton已经算比较完善了,但是依然算不上完美,因为到现在只是解决了多线程问题,加入了模板支持,对于KDL problem(The Dead Reference Problem)依然没法解决,可以说在实现Singleton模式时,最大的问题就是多个有依赖关系的Singleton的析构顺序。虽然Modern C++ Design中给出了解决方案,但是Loki的实现太过复杂,在此就不详细说明了,有兴趣的可以看看Modern C++ Design,当然了,Loki库中用策略模式实现的Singleton也很不错!