网上有很多C++的单例模式的写法
有的把唯一的对象作为一个类的成员变量,有的把唯一的对象放在一个函数里,在第一次调用时创建。
有的GetInstance()返回指针,有的返回引用。
那么在C++有构造函数,拷贝,赋值,以及C++11新增的移动拷贝赋值下,
如何确保单例模式能产生唯一的对象,并且在多线程下不出现错误
网上有很多C++的单例模式的写法
有的把唯一的对象作为一个类的成员变量,有的把唯一的对象放在一个函数里,在第一次调用时创建。
有的GetInstance()返回指针,有的返回引用。
那么在C++有构造函数,拷贝,赋值,以及C++11新增的移动拷贝赋值下,
如何确保单例模式能产生唯一的对象,并且在多线程下不出现错误
2 回答1.2k 阅读✓ 已解决
2 回答1.9k 阅读✓ 已解决
1 回答1.9k 阅读✓ 已解决
1 回答1.9k 阅读
1 回答1.1k 阅读
2 回答1.4k 阅读
1 回答1.6k 阅读
这事还真有点麻烦,大多数网上的单例实现或多或少都有点问题,我们一步步来看。
为了简化讨论,我们假设你不是在写一个运行时靠程序加载的动态库,具体说就是你在写的东西不会被人用
LoadLibrary
(Windows)或者dlopen
(*nix)在运行时动态加载。最简单的单例实现只需要一个全局对象:
这个实现简单到没什么可说,它在绝大多数情况下能正常工作,之所以说是“绝大多数”,主要原因是:
1、它不能保证正确处理依赖性,也就是说,如果你有其它全局对象在构造时依赖于这个对象,你不能保证它能正常工作。
2、构造的时间点其实是不确定的,C++标准只要求这个对象在第一次被使用前,它的构造函数会被调用,具体实现中的保证甚至还达不到这个强度,绝大部分的实现只能保证
main
之后的函数中使用这个对象时,构造函数会被调用。3、这个实现可能会导致你实例化了根本没用到的单例,因为“用到”这个对象的行为不一定能被C++准确识别,很多实现只是简单的在
main
之前按定义的顺序调用所有全局对象的构造函数。所以,既然你在用C++11,那我们就用C++11的新特性来实现这个功能。
在C++11中引入了一个新的东西叫
call_once
,配合once_flag
,你可以保证一个函数只会被调用一次,有了这个东西,创建一个单例的程序可以简化成这样:这段程序在多线程环境里也能正常工作,也能处理单例间的依赖,而且不用在意各种OS/编译器规定的各种坑爹初始化次序,任何编译器,只要它正确实现了C++11标准中的thread部分,就能保证这一点。
单例的销毁其实也是个挺烦问题,最主要的问题是你很难准确的知道你从什么时候开始才真得不再用它,一个简单的思路是
atexit
放一个析构函数在main
之后,但是正如你所预料,简单的实现往往有问题。POSIX的atexit不是线程安全的,你需要用std::atexit
才能保证线程安全。但即使是
std::atexit
也不能正确处理单件之间的依赖,所以我们需要点更精细的办法,比如引用计数。引用计数最大的好处是对象的生存期真正的对应于对象的“有效期”,使用引用计数的版本变成了这样:
全局变量
std::shared_ptr<some_class> the_instance
保证一旦对象创建之后,直到程序结束,它至少有一个活引用,所以不会提前销毁;另外任何依赖于它的对象都会导致引用计数增加,所以可以正确的处理依赖性。当然,这个实现的代价是API接口变成了
std::shared_ptr<some_class>
,因为无论是裸指针还是引用,都不能正确的实现引用计数的语义,如果你对此还是不满意,可以用Boehm GC实现一个兼容裸指针和引用的版本。PS. LoadLibrary/dlopen最大的问题就是它们可能会跳过全局初始化的部分,也就是说,所有“逻辑上”应该在
main
之前执行的部分有可能根本就不会执行,由此会带来各种各样古怪的问题很难在这儿一一详述,总之,避免全局对象+LoadLibrary/dlopen这种组合会极大的改善你的生活品质。