对象和资源的管理
用了这么长时间的 C++ 架构软件,最头痛的莫过于管理内存中的对象和资源。而在管理对象方面,最难处理的就是删除对象的时机。恐怕很多人早就意识到这一点,所以才有了 gc 技术的蓬勃发展。
当一个对象被很多地方引用的时候,通常我们会给出引用记数,当记数减到 0 的时候就删除,这是个看似完美的解决方案。但是,有多少地方会记得解除引用呢?借助 C++ 的语法糖,可以自动的完成这些工作。长期的引用关系,可以在构造和析构的时候操作;短期的引用,比如就在一个函数内获得对象,操作完毕后马上解除引用。这个时候,可以通过返回几个 warpper 对象来完成。
固然,语法糖可以减少出错的可能,但是,代码还是并隐式的加上了,于之伴随的是运行时性能的下降和代码的膨胀。这个时候,多考虑下设计上的改进会有所收获。
最近两年做的项目,我都趋向于对象统一做删除管理。可以在主线程中安排一个定期时间,扫描所有对象,把需要删除的对象删除。而平时的删除,只是做一个简单的 mark 操作。因为 mark 操作是不可逆的,即不能把一个准备删除的对象 mark 回"不必删除"的状态,所以 mark 操作本身是线程安全,不必加锁的。
为什么这样做?因为对于单种对象或是资源而言,大部分情况都是只读的,很多东西经过初始化后,不再有写操作。即使有,也是 os 保证线程安全的。例如文件 handle , socket 这些。而每个对象都会有一种特殊的操作叫做释放,释放可以看成一种写操作,它导致了对象本身的破坏。当一个对象是完全只读的时候,我们就不需要再考虑它的线程安全问题,就是因为有释放这种操作的存在,破坏了完全只读的特性。当我们把释放提取出来统一放在安全的地方处理的时候,我们就可以让大多数对象完全只读,而不用估计线程安全了。
而且这种做法在单线程环境也是有意义的,用一些简单的技巧(例如用一个指针间接访问对象),甚至不用再使用引用计数。
对于单件的处理,采用静态对象和惰性初始化的方案,简直就是 C++ 程序员的陋习。Double Checked Locking is broken 相信很多人都读过了。过于依赖语法糖,通常就会造成这种结果。其实让程序有明显的初始化和退出阶段,是很容易被规划出来的。把单件(singleton) 的处理放在正确的时机,以正确的次序来处理并非难事。
对于全局所有资源的管理,我个人的主张是一定要考虑采用树结构,而不是线性表。因为扫描所有资源这种操作会比较常见。比如以上提到的扫描所有资源,删除 mark 过的对象。采用树结构的好处是,mark 的时候,可以同时 mark 父节点(对父节点计数)。这样,任何资源树上的任何一支都可以通过 root 知道是否需要遍历分支。通常,删除这种操作并不频繁,通过检查根节点一次就可以忽略整个遍历过程了。
而且删除操作往往是可以并行的。为了在删除过程不影响资源树的结构,我们还可以只是对资源树上的节点置空,再统一压缩掉空指针。这样就可以获得最大效率的删除操作,不至于因为定期删除资源而使服务停顿过久。