您的位置:首页 > 编程语言 > C语言/C++

单例分享(循环引用及内存占用解决)

2014-03-03 21:44 531 查看
<分享>单例
HD中由单例引起的Bug,尤其是崩溃Bug已经不止一次了,就在周末我还在解单例相关的Bug。
单例这个问题让我想起了王健上周讲座上的一句话“我们遇到的大多数问题已经被大牛遇到过,总结过,并已经有成熟的解决问题的模式可供参考”。单例这个普通的不能再普通的设计模式,也不是例外。
我的分享很弱,其实就是引用大牛的总结并分享自己的一些心得,希望可以对单例问题的彻底解决有所帮助,虽然以下内容大多是从c++语言的角度介绍的,但其核心思想我觉得对于不同语言是通用的。

简单介绍下我所引用主要的资料(忽略国内很多博客对单例各种遍地生根开花的讨论和介绍,我们需要的是可以能够促进深入理解和可以启迪解决方案的东西):
C++奇才 Andrei Alexandrescu 及其大作《Modern C++ Design》的第六章,Andrei 同时也是loki库作者和facebook folly库的核心开发人员。《Modern C++ Design》中文版为《C++设计新思维——泛型编程与设计模式之应用》,但因书籍毕竟是多年之前的作品,所以最好的参考资料是loki库源码,loki库的源码也是对书中所讲到的各种设计模式的对等实现。
关于单例是什么和简单的单例相关的模式(单例,多例等)不再赘述,进入正题。先介绍Andrei对单例的总结,主要表现在两点:1. 单例的生命周期管理(创建与销毁)2. 单例遇到多线程单例遇到的多数问题都归于这两点,处理好这两点涉及的所有问题,单例出问题的概率就可以降低到很低(至少完全可控) Andrei也对这两点提出了他的解决方案:1. 单例的生命周期管理单例的生命期管理,是单例相关的最复杂,涉及点最多,也是和实际应用相关性最紧密的部分,而其根本原因在于单例之间的依赖关系,降低依赖并理清依赖是解决这个问题的关键,因为在此基础上我们才能决定:A). 单例是需要手动销毁还是交给系统销毁B). 单例的销毁顺序C). 单例是否可以死后重生(凤凰涅磐)。3. 单例遇到多线程多线程是我们常常会遇到的,多个线程访问同一个单例难免会出现,C++中常用的DoubleCheck机制只能减低问题发生的概率,要解决这个问题,必须了解平台相关的并发元素(内存屏障等)。接下来我开始对以上相关问题及解决方式详细的解读:
单例的管理往往跟实际情况是紧密相关的,不可能用一种方式解决所有单例存在的问题,必须分而治之。比如某些单例其生命周期与整个系统都无关,其构造顺序和销毁顺序就可以交给系统。而对于其生命周期有依赖性的,就必须管理其创建及销毁时机,控制其创建及销毁顺序。 空谈的理论没有任何看头,还是看代码,如下单例实现;
Singleton& getInstance(){ static Singleton instance;return instance;} 为Scott meyers 所作,也叫meyer单例,静态全局实例变量实现与其等同。其特点是单例在第一次被使用时才创建,创建于销毁时机都交给编译器决定。显然,它适用于那些生命期与整个系统无关的单例,只要单例与其他单例之间有生存期依赖关系,这种方式就是不合适的,因为我们无法确定到底是哪个先销毁的,也不确定单例被销毁后是否还会被使用。
再看下另一种实作,也是等同于HD c++部分大多单例的实现方式,静态全局指针或静态成员指针的方式实现:
Singleton* getInstance(){ If(! m_instance) { m_instance = new Singleton(); }return m_instance;}void destroy(){ delete m_instance ; }这种方式同样是按需创建,退出时销毁,如果管理好销毁依赖,它在实现上没有大的问题,但会产生另一个问题:
每一个单例都是new出来的,而通常libc对内存分配的实现都是基于内存池的分配和回收,
但是我们知道所new出来的内存一定不止一个单例对象的大小,具体由libc的内存池及内存分配策略实现决定,但是毫无疑问,每个单例对象都hold住了一块堆内存,而如果一个系统中的单例很多,就会出现大量内存消耗,而这种消耗是贯穿整个程序的生命周期的(关于具体占用多少内存,之前作健对android上针对u3相关代码有一份研究报告,可查阅旧邮件)。其解决方案也早已成熟,将单例占用的内存开辟在静态存储区上,并使用placement new/delete 构造和析构,或者编译器通过静态计算获取所有单例及单例所牵连各种成员占用空间的总大小,然后只开辟一块堆内存,使用placement new/delete将所有对象的构造和析构在这一块内存上处理。

关于一个最重要的话题,单例的依赖关系如何彻底解决,一个良好的方式是理出系统中所有的单例及其依赖关系图,通过图我们可以清晰的看出单例之间的依赖关系,当一个新的单例被加入后,我们就把这个新的单例加入整个图,并根据其特点决定其生命周期管理策略。
如下图,可以清晰的看出每个单例的依赖关系,也不难看出S1,S2, S3出现了循环依赖,需要重新思考其设计,实现或者采取某些措施保证这种依赖的安全性;当增加一个单例S7后,我们也可以清楚的知道它对整个单例的系统的影响。【最重要的是可以明确的保证所有的单例都在我们的控制之下,而现在我们常说的往往是“这样应该就没问题了”, “这样可能就没问题了”】





























关于单例的多线程语义,大多是多线程创建与销毁的,只要注意同步就没什么问题了,但如果想在效率上进一步提高,需要借助平台相关的同步机制,这些跟平台相关的轻量级同步机制多是用内嵌汇编的方式实现(对于这些虽然我看过一些实现源码,但是仍旧停留在参考代码上)。

而我自己也有自己的一些想法和问题,分享给大家,看能不能激起什么涟漪或者波涛来:
1. 是不是需要多处访问的地方就需要建模或实现为单例(HD目前代码实现目前存在很多这样的例子),是不是应该更谨慎的考虑怎样的功能或者模块才应该以单例实现,一组静态接口或C接口就可以解决的是否需要或者是否被习惯性的写成了单例? 是否我们把本该是一个单例的功能弄成了一组单例,我们是不是有更优的解决方案.?
2. 单例不仅是c中全局变量的升华,它往往代表着一个独立的功能,并在整个系统中提供统一的访问接口,这样的话,在遇到多线程的情景下,是否可以考虑将其访问控制限制在一个线程之内,其他线程对它的访问也都交到管理线程去?

附注:
单例一般具有单一或有限实例语义,而现实的实现体往往又附加其按序创建语义,在C++代码实现中,我们可能需要有一些在语法角度需要注意的,补充下:
1. 如果需要唯一实例,需要私有化拷贝构造函数和赋值运算符(c++11可直接指定=delete),同时定义构造函数(当你手动实现了拷贝构造函数后,编译器是不会帮你生成构造函数的)。
2. 如果要保证实例不在堆上构建,需要私有化new 操作符, new 操作符的调用先于对象内存分配及构造,其本身带有静态函数属性,但是可不显式指定。
3. 如果要保证实例不在栈上构建,需要私有化析构函数。

希望大家可以集思广益,有分享,有学习,热烈欢迎各位莅临指导。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Design singleton c++语言