#ant

The dreams in which I'm dying are the best I've ever had...

非完美C++ Singleton实现[2]

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 * 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::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也很不错!

posted on 2007-09-07 23:22 蚂蚁终结者 阅读(5025) 评论(13)  编辑 收藏 引用 所属分类: Design Pattern

Feedback

# re: 非完美C++ Singleton实现[2] 2007-09-08 09:12 sneaker

"The Dead Reference Problem"确实是实现Singleton的首要问题  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-09-09 03:25 func

华丽的推演,“裱”成PDF收藏~期待下篇  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-09-10 11:14 ChenA

volatile也解决不了多线程的问题,详情请看C++ and the Perils of Double-Checked Locking。
KDL的问题设计成不依赖不就行了,需要依赖关系的手动释放,这是最简单的办法。  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-09-10 11:33 蚂蚁终结者

@ChenA
thanks!  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-09-19 14:24 func

我的理解,volatile虽然不能解决多线程的很多问题,但这里的使用应该解决了问题.  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-10-09 14:33 fr3@K

已知 : global (static) instance 在 multithreading 下有启始上的问题。
试问 : 如何用一个 global instance (mutex) 去确保另一个 global instance 的启始正确?

请参考拙著 Is your Singleton Broken? (http://fsfoundry.org/codefreak/2006/05/05/is-your-singleton-broken/)  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-10-09 16:30 蚂蚁终结者

@fr3@K
实际上multithreading的问题不在于mutex这里,一个好的设计会在main函数真正启动后再调用Instance(),而这时候global object可以确保已经初始化,即在调用Instance()时可以保证mutex已经初始化。因此只要程序在真正进入main函数以前不调用Instance(),就不会有global instance的初始化问题。  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-10-09 16:32 蚂蚁终结者

其实后来觉得多数情况下eager initialization要优于lazy initialization。
如经典的Meyer's Singleton以及Boost::singleton
  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-10-09 16:42 王博炜

强  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-10-09 21:00 fr3@K

@蚂蚁终结者

我的考量多是站在 library writer (职业病) 的角度。
Library 常需要在程式一 load 上来就把 static (global) instance 启始好. 总不好要求使用者进入 main() 才能使用.

希望你能把进入 main() 之后再呼叫 Instance() 这段加入你的文章里面, 以免误导了读者如我辈.

我是尽量避免 lazy initialization 的人, 尤其是在 static instance (singleton) 的创建上. Lazy initialization 的策略有可能造成譬如说一个 global logger 在某个对象的虚构函数内第一次被使用 (或许可能性很小, 但毕竟不能 100% 排除), 而我们又无法保证该 logger 肯定会被创建成功. 这样是否代表我们必须在每个使用到 logger 的 destructor 内部做 try-and-catch? 等等问题...  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2007-10-10 09:06 蚂蚁终结者

@fr3@K
谢谢你的建议。
不能从static code中调用Instance()确实是一种限制。我想应该可以通过某种类似Boost::singleton中的技巧来确保static object(mutex)在Instance()之前初始化。有时间了再把这段内容补上。  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2008-05-06 09:21 sodar

非常好的文章,学习了

可是遇到一个问题,如果我是在动态库工程中使用这个Singleton,因为atexit不能用于DLL,这种实现是不是会有问题呢?  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2008-05-06 22:02 蚂蚁终结者

@sodar
能说下在动态库工程中使用Singleton的具体需求吗?dll中用atexit注册的函数在FreeLibrary的时候会被调用,可以保证Singleton安全析构。  回复  更多评论   

# re: 非完美C++ Singleton实现[2] 2012-05-28 08:26 SYBILRobbins

This is known that cash can make us autonomous. But what to do when somebody does not have money? The only one way is to try to get the <a href="http://goodfinance-blog.com/topics/business-loans">business loans</a> and financial loan.   回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理