欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

不简单的单例模式Singleton

程序员文章站 2022-06-03 14:58:49
...

单例模式,即Ensure a class only has one instance, and provide a global point of access to it,只有一个实例,是一种非常简单的设计模式,实现的方法有很多种,要完美的实现这种看似简单的设计模式其实不简单。

要实现完美的方案,以下四个问题不能避免:

(1)无内存泄露

首先得是正确的方案,那么不能有内存泄露,这个在程序生命周期内只有唯一一份的实例,在程序退出的时候能正确释放占用的资源(内存、句柄等等)

(2)多线程

很多我们经常用的方案其实不能完美适应多线程程序,主要原因是一些看似原子的操作其实并非原子操作,而是被分解成多个步骤,于是在多线程环境中就会出现问题

(3)性能

有些方案看似能够解决上述两个问题,但是却花费的很大的性能代价,

(4)KDL(Keyboard,Display,Log)problem

        KDL problem解释:某个程序中使用了如下3个Singleton:Keyboard,Display,Log。Keyboard和Display分别对应于计算机的键盘和显示器,Log用来记录错误信息。假设当Keyboard和Display的构造函数和析构函数出现错误时会调用Log记录错误信息,并且构造和析构导致的任何错误都会终止程序。
        在程序启动时,如果Keyboard构造成功,Display构造失败,很显然在Display的构造函数中将会构造Log而且失败信息会被Log记录,根据假设这时候程序准备退出,假设按LIFO的顺序销毁各单例。因为Keyboard先于Log构造,所以Log先于Keyboard析构,但是当由于某种原因Keyboard在析构时失败,想要调用Log记录错误信息时,Log早已被销毁,则Log::Instance()将会导致未定义行为。

          KDL problem其实是指对于有复杂依赖关系的多个Singleton,如何解决最后析构顺序的问题,这个问题很难解决。


尤其是第四个问题,好像很难解决。OK,那下面我们一一分析一些方案,看看他们是不是我们想象中的完美。


写在前面:

        单例类的不同实现方案中,必不可少的是通过将Singleton的构造函数设为private可以禁止客户代码直接创建Singleton对象,除此之外,Singleton的copy constructor和copy assignment operator都为private且仅有声明没有实现,也禁止了客户代码拷贝Singleton对象。唯一可以创建Singleton对象的是Singleton自己的静态成员函数Instance,这样就在编译器保证了Singleton实例的唯一性。上面这些是在C++中实现Singleton模式最基本的要点,即:

  1. private :
  2. Singleton(); // Prevent clients from creating a new Singleton
  3. ~Singleton(); // Prevent clients from deleting a Singleton
  4. Singleton(const Singleton&); // Prevent clients from copying a Singleton
  5. Singleton& operator=(const Singleton& );


后面提到的方案可能为了简单起见没有写完整,但是得需要知道这点。

方案一  静态局部变量:

  1. class Singleton
  2. {
  3. private:
  4. Singleton();
  5. ~Singleton();
  6. public:
  7. static Singleton &instance()
  8. {
  9. static Singleton instance_;
  10. return instance_;
  11. }
  12. };

        在单线程下(或者是C++0X下)是没任何问题的,但在多线程下就不行了,因为static Singleton instance_;这句话不是线程安全的。
         在局部作用域下的静态变量在编译时,编译器会创建一个附加变量标识静态变量是否被初始化,会被编译器变成像下面这样(伪代码):

  1. static Singleton &instance()
  2. {
  3. static bool constructed = false;
  4. static uninitialized Singleton instance_;
  5. if (!constructed) {
  6. constructed = true;
  7. new(&s) Singleton; //construct it
  8. }
  9. return instance_;
  10. }

        这里有竞争条件,两个线程同时调用instance()时,一个线程运行到if语句进入后还没设constructed值,此时切换到另一线程,constructed值还是false,同样进入到if语句里初始化变量,两个线程都执行了这个单例类的初始化,就不再是单例了。

        需要注意的是,C++0X以后,要求编译器保证内部静态变量的线程安全性,可以不加锁。但C++ 0X以前,仍需要加锁:


方案二  静态局部变量+加锁:

  1. class Singleton
  2. {
  3. private:
  4. Singleton();
  5. ~Singleton();
  6. public:
  7. static Singleton &instance()
  8. {
  9. Lock(); // not needed after C++0x
  10. static Singleton instance;
  11. UnLock(); // not needed after C++0x
  12. return instance;
  13. }
  14. };

但这样每次调用instance()都要加锁解锁,性能代价略大。


方案三   静态成员变量:

     那再改变一下,把内部静态实例变成类的静态成员,在外部初始化,因为静态实例初始化在程序开始时进入主函数之前就由主线程以单线程方式完成了初始化,不必担心多线程问题,这样在main函数执行前就初始化这个实例,就不会有线程重入问题了:

  1. {
  2. protected:
  3. static Singleton instance_;
  4. Singleton();
  5. ~Singleton(){};
  6. public:
  7. static Singleton&instance()
  8. {
  9. return instance_;
  10. }
  11. void do_something();
  12. };
  13. Singleton Singleton::instance_; //外部初始化(或者instance_是指针类型,那么在这里就new一个Singleton的对象)

        这被称为饿汉模式,程序一加载就初始化,不管有没有调用到。(对比懒汉模式:即第一次调用该类实例的时候才产生一个新的该类实例,并在以后仅返回此实例。)
        看似没问题,但还是有坑,在一个特殊情况下会有问题:在这个单例类的构造函数里调用另一个单例类的方法可能会有问题。
看例子:

  1. //.h
  2. class Singleton
  3. {
  4. protected:
  5. static Singleton instance_;
  6. Singleton();
  7. ~Singleton(){};
  8. public:
  9. static Singleton&instance()
  10. {
  11. return instance_;
  12. }
  13. void do_something();
  14. };
  15. class CDBOperator
  16. {
  17. protected:
  18. static CDBOperator instance_;
  19. CDBOperator();
  20. ~CDBOperator(){};
  21. public:
  22. static CDBOperator&instance()
  23. {
  24. return instance_;
  25. }
  26. void do_something();
  27. };
  28. Singleton Singleton::instance_;
  29. CDBOperator CDBOperator::instance_;
  30. //.cpp
  31. Singleton::Singleton()
  32. {
  33. printf("Singleton constructor\n");
  34. CDBOperator::instance().do_something();
  35. }
  36. CDBOperator::CDBOperator()
  37. {
  38. printf("CDBOperator constructor\n");
  39. }
  40. void CDBOperator::do_something()
  41. {
  42. printf("CDBOperator do_something\n");
  43. }

        这里Singleton的构造函数调用了CDBOperator的instance函数,但此时CDBOperator::instance_可能还没有初始化。
这里的执行流程:程序开始后,在执行main前,执行到Singleton Singleton::instance_;这句代码,初始化Singleton里的instance_静态变量,调用到Singleton的构造函数,在构造函数里调用CDBOperator::instance(),取CDBOperator里的instance_静态变量,但此时CDBOperator::instance_还没初始化,问题就出现了。
        那这里会crash吗,测试结果是不会,这应该跟编译器有关,静态数据区空间应该是先被分配了,在调用Singleton构造函数前,CDBOperator成员函数在内存里已经存在了,只是还未调到它的构造函数,所以输出是这样:

  1. Singleton constructor
  2. CDBOperator do_something
  3. CDBOperator constructor


方案四   封装静态局部变量:

        单例对象作为静态局部变量有线程安全问题,作为类静态全局变量在一开始初始化,又有以上问题,那结合上述两种方式,得到新的方案是:单例对象作为静态局部变量,但增加一个辅助类让单例对象可以在一开始就初始化。

  1. //.h
  2. class Singleton
  3. {
  4. protected:
  5. struct object_creator
  6. {
  7. object_creator()
  8. {
  9. Singleton::instance();
  10. }
  11. };
  12. static object_creator create_object_;
  13. Singleton();
  14. ~Singleton(){};
  15. public:
  16. static Singleton& instance()
  17. {
  18. static Singleton instance_;
  19. return instance_;
  20. }
  21. void do_something();
  22. };
  23. class CDBOperator
  24. {
  25. protected:
  26. struct object_creator
  27. {
  28. object_creator()
  29. {
  30. CDBOperator::instance();
  31. }
  32. };
  33. static object_creator create_object_;
  34. CDBOperator();
  35. ~CDBOperator(){};
  36. public:
  37. static CDBOperator& instance()
  38. {
  39. static CDBOperator instance_;
  40. return instance_;
  41. }
  42. void do_something();
  43. };
  44. Singleton::object_creator Singleton::create_object_;
  45. CDBOperator::object_creator CDBOperator::create_object_;
  46. //.cpp
  47. Singleton::Singleton()
  48. {
  49. printf("Singleton constructor\n");
  50. CDBOperator::instance().do_something();
  51. }
  52. CDBOperator::CDBOperator()
  53. {
  54. printf("CDBOperator constructor\n");
  55. }
  56. void CDBOperator::do_something()
  57. {
  58. printf("CDBOperator do_something\n");
  59. }

这下可以看到正确的输出和调用了:

  1. Singleton constructor
  2. CDBOperator constructor
  3. CDBOperator do_something

来看看这里的执行流程:
->初始化Singleton类全局静态变量create_object_
->调用object_creator的构造函数
->调用Singleton::instance()方法初始化单例
->执行Singleton的构造函数
->调用CDBOperator::instance()
->初始化局部静态变量CDBOperator instance
->执行CDBOperator的构造函数,然后返回这个单例。


        跟方案三的区别在于QMManager调用QMSqlite单例时,方案3是取到全局静态变量,此时这个变量未初始化,而方案四的单例是静态局部变量,此时调用会初始化。
        跟最初方案一的区别是在main函数前就初始化了单例,不会有线程安全问题。

        这其实就是boost的单例的实现原理,加上模块即可:


方案五  封装静态局部变量+模板化:

  1. //.h
  2. template <typename T>
  3. class Singleton
  4. {
  5. protected:
  6. struct object_creator
  7. {
  8. object_creator()
  9. {
  10. Singleton<T>::instance();
  11. }
  12. inline void do_nothing()const {};
  13. };
  14. static object_creator create_object_;
  15. public:
  16. typedef T object_type;
  17. static object_type& instance()
  18. {
  19. static object_type instance_;
  20. //do_nothing 是必要的,do_nothing的作用有点意思,
  21. //如果不加create_object_.do_nothing();这句话,在main函数前面
  22. //create_object_的构造函数都不会被调用,instance当然也不会被调用,
  23. //可能是模版的延迟实现的特效导致,如果没有这句话,编译器也不会实现
  24. // Singleton<T>::object_creator,所以就会导致这个问题
  25. create_object_.do_nothing();
  26. return instance_;
  27. }
  28. void do_something();
  29. };
  30. //因为create_object_是类的静态变量,必须有一个通用的声明
  31. template<typename T>
  32. typename Singleton<T>::object_creator Singleton<T>::create_object_;
  33. class CDBOperator: public Singleton<CDBOperator>
  34. {
  35. protected:
  36. CDBOperator()
  37. {
  38. printf("CDBOperator constructor\n");
  39. };
  40. ~CDBOperator()
  41. {
  42. printf("CDBOperator destructor\n");
  43. };
  44. friend class Singleton<CDBOperator>;
  45. public:
  46. void do_something()
  47. {
  48. printf("CDBOperator do_something\n");
  49. };
  50. };
  51. int main()
  52. {
  53. CDBOperator::instance().do_something();
  54. return 0;
  55. }

输出:

  1. CDBOperator constructor
  2. CDBOperator do_something
  3. CDBOperator destructor



方案六  智能指针:

        以上五个方案都集中在静态局部变量实现单例这条路上,方案五是这条路上比较好的方案,但是这个方案的问题在于各单例之间析构顺序未定义,如果互相依赖那么可能就有问题,另外方案五属于“饿汉模式”,无法懒加载。这条路基本告一段落,下面我们从其他路来考虑。

        之前有些方案中只有new没有delete,其实memory leak不是主要的问题,所有的现代操作系统在进程结束的时候都会对内存很好的进行回收,比memory leak更值得让人担忧的是resource leak,如果Singleton在构造函数中请求了某些资源:网络连接,文件句柄,数据库连接等,这些资源将得不到释放。
        唯一修正resource leak的方法就是在程序结束的时候delete _instance。用smart pointer再好不过,在这里用auto_ptr就可以满足需要,修改后的代码如下:

  1. class Singleton
  2. {
  3. public :
  4. static Singleton& Instance() // Unique point of access
  5. {
  6. if (0 == _instance.get())
  7. _instance.reset(new Singleton());
  8. return * (_instance.get());
  9. }
  10. void DoSomething(){}
  11. private :
  12. Singleton(){} // Prevent clients from creating a new Singleton
  13. ~Singleton(){} // Prevent clients from deleting a Singleton
  14. Singleton(const Singleton&); // Prevent clients from copying a Singleton
  15. Singleton& operator=(const Singleton& );
  16. private :
  17. friend auto_ptr<Singleton> ;
  18. static auto_ptr<Singleton> _instance; // The one and only instance
  19. };
  20. // Implementation file Singleton.cpp
  21. auto_ptr<Singleton> Singleton::_instance;

        其实这个方案也不完美,除了KDL problem外,还无法完美适应多核多线程环境,原因就在于new操作其实不会死原子操作,后面我们会讲到。


方案七   用atexit替换smart pointer:

        C++并没有规定不同编译单元(translation unit,简单说就是一个可编译的cpp文件)中static对象的初始化顺序。如果一个程序中有多个Singleton对象,那么这些Singleton对象的析构顺序也将是任意的。很显然,当多个Singleton对象有依赖关系时,smart pointer根本无法保证Singleton的析构顺序。即我们开头说的第四个问题,这在大中型程序中不可避免。

msdn中对atexit描述如下:
The atexit function is passed the address of a function (func) to be called when the program terminates normally. Successive calls to atexit create a register of functions that are executed in last-in, first-out (LIFO) order. The functions passed to atexit cannot take parameters. atexit use the heap to hold the register of functions. Thus, the number of functions that can be registered is limited only by heap memory.

        需要说明的是atexit并不比smart pointer好多少,LIFO的保证对于有复杂依赖关系的多个Singleton依然束手无力,但是用atexit替换smart pointer却是必须的,它是设计完美Singleton的基础。


        【为什么atexit为什么还是不行,考虑下面的情况,以KDL问题解释】:
        在程序启动时,如果Keyboard构造成功,Display构造失败,很显然在Display的构造函数中将会构造Log而且失败信息会被Log记录,根据假设这时候程序准备退出,atexit注册的函数将会按LIFO的顺序被调用。因为Keyboard先于Log构造,所以Log先于Keyboard析构,但是当由于某种原因Keyboard在析构时失败,想要调用Log记录错误信息时,Log早已被销毁,则Log::Instance()将会导致未定义行为。

        PS:在我们公司项目中就遇到了以上问题。


        【atexit的严重问题】:
        从上面的例子可以看出,atexit和smart pointer相比仅仅是有LIFO的保证而已,这样的保证貌似也不怎么有效,因为atexit跟smart pointer一样也无法解决KDL probleam。

  1. #include <cstdlib>
  2. void Bar()
  3. {
  4. ...
  5. }
  6. void Foo()
  7. {
  8. std::atexit(Bar);
  9. }
  10. int main()
  11. {
  12. std::atexit(Foo);
  13. return 0 ;
  14. }

        上面的小段代码用atexit注册了Foo,Foo调用了std::atexit(Bar)。当程序退出时,根据atexit的LIFO保证,Bar在Foo之后注册,因此Bar应该在Foo之前调用,但是当Bar注册的时候Foo已经调用了,Bar根本就没有机会能够在Foo之前调用。这明显自相矛盾,因此如果类似代码被调用,肯定不会有什么好的结果,好一点是resource leak,差一点估计程序就崩溃了。

        atexit的这个问题跟Singleton有关系吗?当然有,如果在一个Singleton的析构函数中调用atexit就会出现上述问题。即在KDL problem中,如果Keyboard和Display都构造成功,当Keyboard或Display任意一个析构失败时,Keyboard或Display在析构函数中会构造Log,Log的构造函数会间接调用atexit——可怕的未定义行为。

        看到这里你一定对atexit相当失望,貌似它带来的好处多于坏处。但是请你相信,如果适当设计,atexit在后面的Singleton改造中会起到很重要的作用。

  1. class Singleton {
  2. public :
  3. static Singleton& Instance() // Unique point of access
  4. {
  5. if (0 == _instance)
  6. {
  7. _instance = new Singleton();
  8. atexit(Destroy); // Register Destroy function
  9. }
  10. return * _instance;
  11. }
  12. void DoSomething(){}
  13. private :
  14. static void Destroy() // Destroy the only instance
  15. {
  16. if ( _instance != 0 )
  17. {
  18. delete _instance;
  19. _instance = 0 ;
  20. }
  21. }
  22. Singleton(){} // Prevent clients from creating a new Singleton
  23. ~Singleton(){} // Prevent clients from deleting a Singleton
  24. Singleton(const Singleton&); // Prevent clients from copying a Singleton
  25. Singleton& operator=(const Singleton& );
  26. private :
  27. static Singleton *_instance; // The one and only instance
  28. };
  29. // Implementation file Singleton.cpp
  30. Singleton* Singleton::_instance = 0;

         考虑Destroy中的_instance = 0;这一行代码,上述代码实际上实现的是不死鸟模式(The Phoenix Singleton),所谓不死鸟,就是可以死而复生。上面的代码可以解决本文最早提出的KDL problem,即如果Keyboard析构失败,虽然Log已经析构,但是由于Destroy中的_instance = 0;这一行代码,Log::Instance()创建一个新的Log对象后,程序将会表现良好。当然了,Phoenix Singleton仅能用于无状态的Singleton,如果Log需要保存某些状态,Phoenix Singleton也不会带来任何好处。你当然可以用某些方法维持Phoenix Singleton的状态,但是在做之前先想想看是否值得,维持状态可能会使Singleton变得特别复杂。

        上面的Phoenix Singleton已经可以满足大部分需要,如果你的Singleton没有涉及到多线程,多个Singleton之间也没有依赖关系,你大可以放心使用。但是如果你用到多线程,或者你的Singleton关系如KDL般复杂,或者你觉得对每一个Singleton都敲同样的代码让你厌烦,那再看下面的方案。


方案八   适应多线程:

        一般都会考虑加锁:

  1. //.h
  2. class Singleton
  3. {
  4. public:
  5. static Singleton& Instance() // Unique point of access
  6. {
  7. Lock lock(_mutex);
  8. if (0 == _instance)
  9. {
  10. _instance = new Singleton();
  11. atexit(Destroy); // Register Destroy function
  12. }
  13. return *_instance;
  14. }
  15. void DoSomething(){}
  16. private:
  17. static void Destroy() // Destroy the only instance
  18. {
  19. if ( _instance != 0 )
  20. {
  21. delete _instance;
  22. _instance = 0;
  23. }
  24. }
  25. Singleton(){} // Prevent clients from creating a new Singleton
  26. ~Singleton(){} // Prevent clients from deleting a Singleton
  27. Singleton(const Singleton&); // Prevent clients from copying a Singleton
  28. Singleton& operator=(const Singleton&);
  29. private:
  30. static Mutex _mutex;
  31. static Singleton *_instance; // The one and only instance
  32. };
  33. //.cpp
  34. Mutex Singleton::_mutex;
  35. Singleton* Singleton::_instance = 0;

现在的Singleton虽然多线程安全,性能却受到了影响。从Instance()中可以看到,实际上仅仅当0 == _instance为true时才需要Lock。故改为:

  1. static Singleton& Instance()
  2. {
  3. if (0 == _instance)
  4. {
  5. Lock lock(_mutex);
  6. _instance = new Singleton();
  7. atexit(Destroy);
  8. }
  9. return *_instance;
  10. }

但是这样还是会产生竞争条件(race condition),一种广为人知的做法是使用所谓的Double-Checked Locking

  1. static Singleton& Instance()
  2. {
  3. if (0 == _instance)
  4. {
  5. Lock lock(_mutex);
  6. if (0 == _instance)
  7. {
  8. _instance = new Singleton();
  9. atexit(Destroy);
  10. }
  11. }
  12. return *_instance;
  13. }

        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


方案九   将上述Singleton泛化为模板

  1. template<class T>
  2. class Singleton
  3. {
  4. public:
  5. static T& Instance() // Unique point of access
  6. {
  7. if (0 == _instance)
  8. {
  9. Lock lock(_mutex);
  10. if (0 == _instance)
  11. {
  12. _instance = new T();
  13. atexit(Destroy);
  14. }
  15. }
  16. return *_instance;
  17. }
  18. protected:
  19. Singleton(){}
  20. ~Singleton(){}
  21. private:
  22. static void Destroy() // Destroy the only instance
  23. {
  24. if ( _instance != 0 )
  25. {
  26. delete _instance;
  27. _instance = 0;
  28. }
  29. }
  30. static Mutex _mutex;
  31. static T * volatile _instance; // The one and only instance; 加volatile关键词防止编译器优化
  32. };
  33. template<class T>
  34. Mutex Singleton<T>::_mutex;
  35. template<class T>
  36. T * volatile Singleton<T>::_instance = 0;

测试代码:

  1. #include "singleton.h"
  2. class A : public Singleton<A>
  3. {
  4. friend class Singleton<A>;
  5. protected:
  6. A(){}
  7. ~A(){}
  8. public:
  9. void DoSomething(){}
  10. };
  11. int main() {
  12. A &a = A::Instance();
  13. a.DoSomething();
  14. return 0;
  15. }

        此算比较完善了,但是依然算不上完美,因为到现在只是解决了多线程问题,加入了模板支持,对于KDL problem(The Dead Reference Problem)依然没法解决,可以说在实现Singleton模式时,最大的问题就是多个有依赖关系的Singleton的析构顺序。有兴趣的可以看看Modern C++ Design给的解决方案,Loki库中用策略模式实现的Singleton也很不错。











参考资料:

http://blog.cnbang.net/tech/2229/

http://blog.csdn.net/kikikind/article/details/2328768



原文地址:https://blog.csdn.net/yockie/article/details/40790347