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也很不錯!