说到 C/C++ 的资源管理,人人都会头痛半天。自从 C++0x (就是 C++09 了)标准漏出风声之后, C++ 标准是否会引入自动垃圾回收机制就成为了众多 C++ 爱好者谈论的话题。但是实际上,在 C++ 标准的探索上,垃圾回收一直处在一个十分低下的地位。造成其这一处境的原因很多,也很复杂。我们来看看站在 C++ 程序员的角度上看,资源管理机制现在所面临的局势。
从系统结构上来讲, C/C++ 支持 3 种内存管理方式,基于栈的自动管理,基于堆的动态管理,和基于全局区的静态管理。由于 RAII 的理念,对于 C++ 来说,内存管理和其他资源管理本质上没有区别。因此对于资源而言,也就自然的拥有这样 3 种管理方式。
首先简要的介绍一下 RAII 。 RAII 的全称是 Resource Acquisition Is initialization 。这个思想的基本手法是对于一种想要使用的资源,为其书写一个 guard 类,在该类的构造函数里进行资源的请求,在析构函数里进行资源的释放。例如假设我们想管理一个互斥锁,可能的方式是:
struct lock_guard
{
lock_guard() { lock ();}
~ lock_guard() {unlock();}
} ;
此后,对这个对象使用什么内存管理方式,也就等价于对这个互斥锁使用什么内存管理方式。
借助于 RAII ,以后我们可以只讨论内存资源的管理方式,其它资源的管理方式可以使用 RAII 来同样的实现。现在我们已经很自然的获得了资源管理的 3 种方式:基于堆的动态方式、基于栈的自动方式和全局。值得一提的是,这 3 种方式中比较不容易出错的后两种实际上可以解决大部分的资源管理需求。因为绝大部分资源,都属于获取 - 使用 - 释放型的,例如很多同步对象,文件锁, WinGDI 里的许多 GDI 对象。我们缺乏管理的,只有那些一次获得,多个环境拥有,并且只能有一次释放的少数资源。
回到内存模型来看,有一点让我们无法将内存与其它资源等同(反过来,把其它资源和内存等同却是可以的),那就是循环引用。 A 内存可以持有指向 B 内存的引用, B 内存也可以反过来持有 A 内存的引用。循环引用导致内存管理不可以用“是否有指向该内存的引用”来区分一块内存是否可以回收。从而丧失了一个绝佳的管理手段。但是在没有循环引用的场合下,我们还是有非常简洁高效的管理方法的。那就是引用计数。
引用计数是在没有循环引用场合下进行内存管理的绝佳手段,它具有轻量、高效、即时、可控的优点。而且在 C++ 里,引用计数已经非常成熟,只需要使用 boost.shared_ptr 或者其它非官方的引用计数指针库就可以了,而且据悉 C++09 很可能把 boost.shared_ptr 纳入标准库。引用计数的原则是,如果一个对象没有别的指针或引用来指向它,那么这个对象就是可以释放的。具体的手法有大把大把的资料可以查阅,这里就不详细说明了。引用计数通常可以处理哪些场合的资源管理问题呢?首先,对于单方向的资源管理,也就是多个 A 的实体拥有 1 个 B ,然而 B 并不会反过来依赖于 A (例如多个对象共享一个日志),引用计数是非常合适的。其次,对于拥有 - 反作用的场合,也就是 1 个或多个 A 的实体拥有 1 个或多个 B ,而 B 也拥有这些 A 的实体的引用,但是 B 的生存期仍然决定于 A 的生存期(例如父窗口拥有若干子窗口,子窗口也具有 parent 指针指向父窗口,但是子窗口的生存期决定于父窗口的生存期),这个时候 A 可以对 B 使用引用计数指针,而 B 可以对 A 使用原生的普通指针,同样的可以很好的解决问题。
现在所剩下的,就只有生存期的循环依赖了。如果 AB 互相持有对方的引用,而且 AB 互相的存在都依赖于对方,这样引用计数就无法解决了。但是如果仔细想一下就会发现,这种情况在 C++ 里几乎不可能存在。生存期循环依赖只有 2 种后果,要么 A 和 B 的析构函数里互相析构(当然就挂了),要么互相都不析构(当然就泄露了)。而这两种都是在正常编程中不会出现的情况。所以如果即使仅仅使用引用计数,我们也可以解决几乎所有的资源管理问题。
现在还剩下那么一丁点极少出现的不能处理的情况,我们可以使用更加复杂的 gc 来实现。可惜的是,实现一个 gc 所要耗费的精力实在太大,而且几乎不可避免的要成为侵入式的库。所以有点得不偿失。而且 gc 通常会产生更多的毛病:
1. 你无法却知对象析构的具体时间,从而无法真正知道影响程序性能的瓶颈在什么地方。
2. gc 都倾向于大量的使用内存,直到内存不够的时候再进行清理,这样会导致程序的内存用量严重颠簸,并且产生大量的换页。
3. 过度的依赖于 gc 会使程序员大量的把可以由之前提到的各种方法来处理的资源交给 gc 来处理,无故的加重了 gc 的负担。
4. gc 的管理方法和 C++ 的析构函数有可能产生语义上的冲突。
这就是为什么 C++ 标准对垃圾回收的态度如此恶劣的原因。
我们现在回过头来看 Java/C# 这样的内置 gc 的语言。这样的语言由于使用了 gc ,就不可避免的放弃了析构函数。为什么 gc 会和析构函数产生冲突呢?一个 gc 一般会希望在进行垃圾回收的时候,整个过程是一个原子的,但析构函数会破坏这一点,在释放内存的时候如果还要执行代码,那么难免会对整个 gc 环境产生破坏性的影响。由于没有析构函数,这些语言就不可能做到 RAII ,也就是说,它们的 gc 所能够管理的,也就仅仅只有内存而已了。对于其他资源, Java 等就必须手动释放。虽然 C# 提供了 with 关键字来缓解这一问题,但仍然无法彻底的解决。
还有什么麻烦呢?之前说的那 4 点全部都有。虽然 JVM 的速度在不断的提高,但是内存使用这一点却完全没有发展,不能不说是 gc 说导致。它所带来了什么好处呢?是内存管理的自动化,而不是资源管理的自动化。
所以说 C++ 并不是世人所想象的那样需要 gc , C++ 本身就已经提供了足够强大的资源管理能力。基于栈的自动管理,或者使用引用计数,几乎可以达到和 gc 同样的覆盖面,而且没有 gc 的那些问题, RAII 使得 C++ 在管理非内存资源的时候还更加有优势,为什么不使用呢?
ps. 设计一个非官方的 gc 库还是可以的。但是毕竟不会成为主流了。
posted on 2007-01-24 18:02
shifan3 阅读(2116)
评论(4) 编辑 收藏 引用 所属分类:
C++