现在的位置: 首页 > 综合 > 正文

RAII和垃圾收集(下)

2013年08月14日 ⁄ 综合 ⁄ 共 2762字 ⁄ 字号 评论关闭

上回说到,RAII 与现有的 GC 环境互不相容,也提到了问题的症结在于对析构函数的调用。这并非仅仅是一个令人遗憾的巧合,仔细想想不难发现,在这个矛盾背后,实际上是两者在“如何看待一个对象”这一问题上的分歧。

前面说过,RAII 的核心内容是把资源托管给对象,并保证资源在对象生命周期内始终有效。这样,我们实际上把管理资源的任务转化成了管理对象的任务,你拥有对象就等于拥有资源,对象存在则资源必定存在,反之亦然。RAII 的全部威力均源于此。这种做法的出发点,用 Bjarne Stroustrup 的话来说,是“use an object to represent a resource”(引自本文上篇开头所引用的代码中的注释)。换句话说,在 RAII 的眼中,对象代表了资源,它的行为和状态应该与资源的行为和状态完全一致,所以资源的申请和释放也就自然应该和对象的构造与析构对应起来。对于大多数资源,程序员有权而且需要控制它何时释放,那么很自然的,对于管理这些资源的对象,程序员也就应该有权而且需要控制它何时析构。一言以蔽之,这要求程序员有权控制对象的生命周期。

然而现有的 GC 环境并不满足上述条件。在本文的上篇中介绍过,对于 GC 来说,一个对象的生命周期有多长程序员是无权干涉的,任给对象 A,它是否存活必须严格地按照“是否存在一条从根出发的指针链能够到达 A”来判定。在 GC 的眼中,对象只是一段被分配的内存,而历史已经证明,决定内存何时释放这个任务决不能交给爱犯错的程序员,应该由专门的收集器完成,以确保每个内存回收动作都是安全的。什么?一个对象代表一份资源?哈!我 GC 大人的眼中只有内存,没有什么资源!

啧啧,两者眼中对象的角色差异如此之大,难怪会水火不容了。不过,这个矛盾真的没有调和的可能吗?倒也不见得。RAII 看重对象的语义,要让对象代表资源,因此需要程序员能够控制对象的生命周期,而 GC 眼中只有内存,反复强调只有让收集器来回收内存才能保证安全,这活不能让程序员来干。因此,要同时满足这两者的要求,对象的生命周期和必须和内存的释放脱钩。这不是什么新鲜想法。事实上,按照 C++ 标准的定义,一个对象的生命周期始于构造函数,终于析构函数,它本来就不是由内存分配释放来决定的,而 placement new 更是把这一点表现的极为充分,分配内存、构造对象、析构对象、释放内存四个操作清清楚楚、各自独立。至此,我们的解决方案已经呼之欲出了。一方面,我们要把释放内存的责任移交给 GC,同时又要允许程序员控制对象生命周期,那么,正确的做法就是 ……

在现有 GC 机制的基础上,允许程序员显式地析构对象,同时,若程序试图访问已经析构的对象,将抛出异常。

这初看起来非常荒谬(我已经可以看到台下飞来的无数鸡蛋和番茄 …… 哎哎?!你们真砸啊?),但其实并非如此。首先,内存的分配和释放由 GC 机制管理,凡是指针可到达的内存总是有效的,因此我们拥有同样的安全性保证 —— 没有悬挂引用和内存泄漏,所以,我们可以放心大胆地在模块之间共享对象,不必操心悬挂引用的问题,也不必担心内存管理的细节“弄脏”模块之间的接口;其次,由于允许显式析构对象,程序员能够控制对象的生命周期,因此我们可以继续应用 RAII “用对象代表资源”的语义并享受它带来的便利;最后,试图访问已经析构的对象将会抛出异常,堵上了无定义行为的口子。一句话,现在我们可以把对象放入支持 GC 的堆里而不必改变它的语义了。

瞧!两边不是合作得很好么?

必须承认的是,要实现这个方案,实际上是非常简单的,但是就我所知,目前并没有任何一个 GC 环境支持这个做法。这是因为大多数支持 GC 的语言并没有类似 C++ 中的析构函数这样的语言特征,换句话说,压根没有应用 RAII 的能力,而 C++ 本身又并不支持 GC。嗯嗯,面对这种情况,我决定效法先贤 —— 自己实现一个用于 C++ 的垃圾收集库,用智能指针来实现对指针访问的控制,并提供显式析构的接口。

最后,我来试着回答某些可能出现的质疑:

问:显式析构和显式调用 close 有什么区别?不一样要程序员自己动手吗?
答:有区别。请参阅本文上篇中,关于 RAII 相对于显式释放资源方案的优势。

问:你允许程序员析构对象,那么可能出现指向已析构对象的指针,这和悬挂指针不是一样么?
答:不一样。如果你试图访问已析构对象,只会抛出一个异常(当然,这得通过智能指针来实现),而访问悬挂指针 …… 不用我多说吧?

问:(续上)这样一来,通过指针访问对象可能抛出异常,这是原来没有的。
答:这种说法不准确。在现有的 GC 机制下,对象占有的资源需要调用(例如)close 成员函数显式释放,而在调用了 close 之后,再试图访问这个对象一样会抛出异常,指明使用这个对象所需的资源已经被释放。而且,对于现有的方案,每种对象释放资源的函数可能都不同,标识“资源已释放”的异常可能都不同,但如果使用显式析构的方案,释放资源的手段是一致的(析构对象),标识“资源无效”的异常也是一致的(“对象已析构”),这大大减轻了程序员的簿记负担。

问:(再续)但是你多一个检查啊?每个指针访问都需要检查对象是否已析构,效率太低。
答:这种说法也不准确。采用显式调用 close 释放资源的方案,在该对象内部,每个完成实质性工作(因此需要访问对象所需的资源)的成员函数同样必须检查资源是否有效,也是一个检查。而且,这样的检查程序员需要为每个类分别撰写,而采用上述显式析构的方案,只有智能指针需要做这个检查。

问:(再续)你上述两个问题的回答多少有点偏颇吧?并不是对象的每个成员函数都需要访问资源,你上述两个辩解对于不需要访问资源的成员函数是不成立的。而且,也并不是所有的对象都需要管理资源,如果我想在支持 GC 的堆里放一个 int 怎么办?这样,“可能会抛异常”和“多一个检查”无疑是两个很大的缺点了吧?
答:嗯嗯,这可能是最有力的质疑了,我的回答是这样的:我会另外提供一种不支持显式析构的智能指针,它的使用就像传统 GC 环境下的指针一样,也就没有“可能会抛异常”和“多一个检查”的问题。换句话说,如果你的对象不管理资源,或者你一定要在资源释放之后还能继续访问对象,用它你可以退回到传统的 GC 方案去。

问:我就是不喜欢你这套想法!有意见吗?
答: …… 没意见。如果你确实无法接受“显式析构”的想法,你也可以只使用上面提到的另一种智能指针 —— 嗯嗯,除了“显式析构”之外,我对于如何在 C++ 中具体实现垃圾收集机制也有很多其他的想法,除了“显式析构”之外,我的收集器应该还会有其他方面的优点。当然,这是其他文章的内容了。

好,大致就是这样,希望大家多提意见。如果哪位对实现垃圾收集器这个具体工作有兴趣,请来这边坐坐:
http://www.allaboutprogram.com/bb/viewtopic.php?t=1520

抱歉!评论已关闭.