RAII和垃圾收集(上)

原创 2004年02月18日 11:23:00

先来看一小段代码,它取自 Bjarne Stroustrup 的演讲“Speaking C++ as a Native”:

// use an object to represent a resource ("resource acquisition is initialization")

class File_handle { // belongs in some support library
    FILE* p;
public:
    File_handle(const char* pp, const char* r)
        { p = fopen(pp,r); if (p==0) throw Cannot_open(pp); }
    File_handle(const string& s, const char* r)
        { p = fopen(s.c_str(),r); if (p==0) throw Cannot_open(pp); }
    ~File_handle() { fclose(p); } // destructor
    // copy operations and access functions
};

void f(string s)
{
    File_handle file(s, "r");
    // use file
}

熟悉 C++ 的朋友对这种简称为 RAII 的技巧一定不会陌生。简单的说,RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:

第一、我们不需要显式地释放资源。以上述代码中的函数 f 为例,我们不必担心“忘记”关闭文件的问题。而且,即使是函数 f 的控制结构发生了改变,例如在函数中间插入 return 或者抛出异常,我们也能确定这个文件肯定会被关闭。特别是在“异常满天飞”的如今,RAII 是实现异常安全的有力武器。类似的,如果某个类 C 包含一个 File_handle 成员,我们也不必担心类 C 的对象会在销毁时“忘记”关闭文件。

第二、采用这种方式,对象所需的资源在其生命期内始终保持有效 —— 我们可以说,此时这个类维护了一个 invariant。这样,通过该类对象使用资源时,就不必检查资源有效性的问题,可以简化逻辑、提高效率。

好,介绍完了 RAII,下一个要出场的角色是大名鼎鼎的垃圾收集(Garbage Collection,下面简称 GC)。随着 Java 的流行,GC 已经被越来越多的人所接受,下面我简单介绍一下 GC 的运行机理。

首先引入几个术语:在 GC 的语境中,对于程序可以直接操纵的指针值(例如,保存在局部变量或是全局变量中的),我们称之为“根”;假设对象 A1 保存了一个指向对象 A2 的指针,对象 A2 保存了指向对象 A3 的指针,我们称 A1->A2->A3 构成了一条“指针链” —— 当然,指针链可以任意地长;假设从程序中的某个根出发,通过一条指针链能够到达对象 A,那么我们认为对象 A 是“存活”的,否则,就认为它已经“死亡”,随时可以释放它占用的内存。

所有 GC 实现,其运行方式都是检查对象是否存活,并将已经死亡的对象释放,其实现机理一般分为三大类:一、引用计数(reference counting),这类 GC 实现为每个对象保存指向它的指针数量,一旦这个数量降为 0 ,就将这个对象释放,小有名气的 boost::shared_ptr 采用的就是就是这种方式;二、标记-清扫(mark-sweep),这类 GC 实现周期性地扫描整个堆,先将其中的存活对象标记出来,然后再将剩下的死亡对象全部释放;三、节点复制(copying),这类 GC 实现将整个堆分成两半,并周期性地将存活对象从当前使用的那一半搬到另一半,留在原先位置的死亡对象就自然地被抛弃了。这三类实现中,引用计数的限制最多(特别是无法回收环形结构),而且一般在效率上居于劣势,应用较少,后两类使用较多。这方面的一些细节,可以参考 2003 年第 1 期程序员上的垃圾收集专栏。另外,人民邮电出版社即将推出《垃圾收集》一书的中译本,这本书可以说是目前世上唯一一本关于 GC 的全面性的专著,对 GC 有兴趣的朋友可以找来看一下(嘻嘻,打个广告,^_^)。

毫无疑问,对于程序员来说,在分配了内存之后如果能够不必操心怎么释放它,那一定是非常惬意的。更重要的是,程序员们从此可以向悬挂引用和内存泄漏告别了 —— 它们可是程序开发中最令人头痛的 bug 之一。最后,有了 GC 的支持,在程序的各个模块之间共享数据变得更容易、更安全,有助于简化模块之间的接口。虽然在 GC 对效率的影响方面,人们还存在着各种疑虑,但必须承认,GC 是一种有价值的技术。

可惜,非常不幸的,现有的 GC 机制和 RAII 之间可以说是水火不容 —— 怎么会这样呢?

症结在于这两位对待析构函数的态度不同。回顾我们对 RAII 的介绍,它的核心内容就是将一份资源托管给一个对象,让资源在对象的生命周期之内均处于有效状态,这样,它就要求资源由对象的析构函数来释放。而问题正是在于,当前现有的 GC 机制下面,很难提供对析构函数的支持。可能会有人感到奇怪,让 GC 实现在释放对象的时候调用析构函数不就结了吗?可惜,事情不那么简单。

在 GC 的语境中,像析构函数这样在销毁对象时执行的动作,被称为“终结”(finalization),而支持终结一直是 GC 实现上的一个难题,因为终结动作很可能给收集工作带来很大的干扰。举例而言,考虑下面这样一个终结动作(这里我采用 C++ 析构函数的形式):

class wedget
{
    ... ...
    ~wedget()
    {
        // global_pointer 是一个全局变量
        global_pointer = this;
    }
};

假设现在我们有一个 wedget 对象 w,进一步假设在某个时刻,GC 机制发现从任何一个根出发,都无法到达 w ,那么按照定义它已经死亡,可以执行终结动作然后释放了。但是,当我们执行终结动作的时候,w 又把指向自己的指针赋给了一个全局变量也就是一个根,也就是重新出现了一条由根出发、可到达 w 的指针链,这样,按照定义 —— 它又复活了!如果你有心,随便动动脑子就可以想出上述问题的许多变种,其中有一些还可能显得很“冠冕堂皇”。

此时我们该怎么做呢?复活 w ?那样的话我们还必须复活所有 w 指向的对象,但要实现这一点很难,这要求我们不能在执行终结动作之前释放任何对象(你无法预先确知终结动作会影响哪些对象),而且可能陷入死循环(执行完终结动作之后,你必须重新确定各个对象存活与否,然后再试着执行终结动作 ……)。那么我们不复活 w ?也不好,这样一来 global_pointer 就成了一个悬挂引用,GC 保证的安全性就被捅了一个大窟窿。或者我们禁止在析构函数中出现指针操作?困难,如果析构函数调用其他函数,难道你还能递归地禁止下去?要不我们禁止调用其他函数?咳咳,那这个析构函数根本就无法实现任何实质性的功能,不要提释放资源了。

除去实现上的困难之外,用 GC 中的终结机制来释放资源还有一个更本质上的问题:执行终结机制的时间是无法确定的。且不说除引用计数之外的 GC 实现释放对象本来就有相当大的延时,就算将来的实现真的能够保证对象在死亡的瞬间被释放,同样无法满足需求:假设在某一时刻你希望析构某个对象,释放它占有的资源,但只要某处仍然存在一个指向该对象的指针,这个对象就会“顽强”地生存下去。不妨假设一下,如果这里需要释放的资源是一个计时收费的网络链接,那么 …… (祝你好运,兄弟!这是你的铺盖卷,^_^)

综上,我们已经有充分的理由说,现有 GC 环境下面根本不可能应用 RAII ,它们之间水火不容。事实上,像 Java 那样支持 GC 的语言,一般都不鼓励你使用终结机制,对象所需的资源必须显式地释放。最简单的,为这个类添加一个 close 成员函数负责释放资源。

这样做有什么缺点呢?对照最初我们对 RAII 优点的介绍就可以知道了:

首先,所有对象需要的资源必须显式地手工释放。拿最初的例子来说,函数 f 的最后必须加上一句 file.close(),而且我们得开始担心函数 f 控制结构的改变,无论是中间插入 return 还是可能抛出异常的地方,都必须加上 file.close()。针对这种情况,Java 等语言一般会支持 try ... finally 这个特征,规定无论因为何种原因离开函数,都必须调用 finally 代码块中的代码。try ... finally 确实有效地缓解了这一问题,但是仍然不及 RAII 方案理想:第一、在撰写 try ... finally 中付出的努力是无法重用的,如果你有 10 个函数里用了 file_handle,你必须把同样的代码写上 10 遍;第二、确保  try 块中申请的资源和 finally 块中释放资源互相配对现在成了程序员的责任,这是多出来的簿记负担,而且一旦出错出现资源泄漏是很令人头痛的 —— 一般来说,这要比内存泄漏隐蔽多了,而且不可能有专门的工具帮忙;第三、如果某个类拥有若干类似 file_handle 这样的成员,我们必须为这个类也添加一个 close 函数,并逐个调用成员的 close 函数(搞不好各个成员释放资源的函数名字还不一样),这也是一个多出来的簿记负担 —— 而且 try ... finally 帮不上什么太大的忙。

其次,由于允许显式地释放资源,对象无法再像以前那样保持“所需的资源在其生命期内始终有效”这样一个 invariant ,因此对象中所有使用资源的方法必须检测资源的有效性,用户在使用对象的时候也必须留意资源是否有效(这种错误多半以异常形式呈现)。这不仅使得逻辑变得复杂,而且又是一个多出来的簿记负担 —— 对于每种需要资源的类,用户必须记住它们抛出的代表资源无效的异常是什么。

唔 ~~ 到目前为止,似乎形势稍稍有点令人沮丧。RAII 是管理资源的利器,而 GC 提供的方便和安全保证更是诱人之极,但偏偏两者不可得兼。你要么投向 GC 的怀抱,然后不得不手工管理其他资源,忍受多出来的麻烦和簿记负担;要么放弃 GC,老老实实手工管理内存,但却能够在管理其他资源的时候享受 RAII 带来的方便和安全。你当然可以说世界就是这样的,有时候我们不得不做出权衡,为了得到一些而放弃另一些,只是 …… 有没有更好的办法呢?

锵!锵!啪!

欲知后事如何,且听下回分解!

RAII和垃圾收集(上)

 选择自 Elminster 的 Blog先来看一小段代码,它取自 Bjarne Stroustrup 的演讲“Speaking C++ as a Native”:// use an object t...
  • stuart_zhu
  • stuart_zhu
  • 2006年01月17日 11:30
  • 447

RAII和垃圾收集

Author:Elminsterhttp://blog.csdn.net/Elminster/先来看一小段代码,它取自 Bjarne Stroustrup 的演讲“Speaking C++ as a ...
  • zheng80037
  • zheng80037
  • 2007年05月29日 15:51
  • 338

RAII和垃圾收集(下)

上回说到,RAII 与现有的 GC 环境互不相容,也提到了问题的症结在于对析构函数的调用。这并非仅仅是一个令人遗憾的巧合,仔细想...
  • stuart_zhu
  • stuart_zhu
  • 2006年01月17日 11:32
  • 753

RAII和垃圾收集GC

RAII和垃圾收集GC,今天无意中看到了RAII,就找到了这篇文章,写的很好,值得一读!...
  • MONKEY_D_MENG
  • MONKEY_D_MENG
  • 2010年07月13日 15:45
  • 1385

C++之RAII技术解析

1.什么是RAII 技术? 我们在C++中经常使用new申请了内存空间,但是却也经常忘记delete回收申请的空间,容易造成内存溢出,于是RAII技术就诞生了,来解决这样的问题。RAII(Resour...
  • doc_sgl
  • doc_sgl
  • 2015年01月22日 22:51
  • 5644

RAII和智能指针的实现

RAII在C++effective一书中讲到,RAII是“Resource acquisition is initialization”,直译为“资源获取就是初始化”。它是基于这样的原理,栈的变量会自...
  • xy913741894
  • xy913741894
  • 2017年04月05日 12:14
  • 239

RAII的使用

C++中的RAII全称是“Resource acquisition is initialization”,直译为“资源获取就是初始化”。但是这翻译并没有显示出这个惯用法的真正内涵。RAII的好处在于它...
  • fcb_campnou
  • fcb_campnou
  • 2015年03月23日 19:50
  • 253

C++11实现模板化(通用化)RAII机制

什么是RAII?RAII(Resource Acquisition Is Initialization),也称直译为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的机制。 C++标准保...
  • 10km
  • 10km
  • 2015年11月15日 10:04
  • 2891

C++ —— RAII编程思想

RAII则是在C++项目中用于资源管理的一种重要的编程思想。
  • noahzuo
  • noahzuo
  • 2016年04月13日 07:39
  • 918

C++之 RAII基本理解与使用

产生原因:      在C++中,如果在这个程序段结束时需要完成一些资源释放工作,那么正常情况下自然是没有什么问题,但是当一个异常抛出时,释放资源的语句就不会被执行。于是Bjarne Stroust...
  • My_heart_
  • My_heart_
  • 2016年09月06日 18:28
  • 344
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:RAII和垃圾收集(上)
举报原因:
原因补充:

(最多只允许输入30个字)