RAII 与垃圾收集器
RAII vs. Garbage Collector
我最近在 CppCon 2016 上观看了 Herb Sutter 关于 "Leak Free C++..." 的精彩演讲,他谈到了使用智能指针来实现 RAII(资源获取即初始化)——概念以及它们如何解决大部分内存泄漏问题。
现在我在想。如果我严格遵守 RAII 规则,这似乎是一件好事,为什么这与在 C++ 中使用垃圾收集器有什么不同?我知道使用 RAII 时,程序员可以完全控制何时再次释放资源,但是在任何情况下,仅拥有垃圾收集器是否有益?真的会效率低吗?我什至听说有一个垃圾收集器可以更有效,因为它可以一次释放更大的内存块,而不是在整个代码中释放小块内存。
垃圾收集器的一个问题是很难预测程序性能。
使用 RAII,您知道在准确的时间资源将超出范围,您将清除一些内存,这需要一些时间。但是,如果您不精通垃圾收集器设置,则无法预测何时会进行清理。
例如:使用 GC 可以更有效地清理一堆小对象,因为它可以释放大块,但操作速度不快,而且很难预测何时会发生 in,因为 "large chunk cleanup" 这将占用一些处理器时间并会影响您的程序性能。
粗略地说。对于 latency 和 jitter,RAII 惯用法可能更好。对于系统的 吞吐量 .
,垃圾收集器可能更好
请注意,RAII is a programming idiom, while GC 是一种内存管理技术。所以我们正在比较苹果和橙子。
但我们可以将 RAII 限制在其内存管理方面仅并将其与 GC 技术进行比较。
所谓的基于 RAII 的内存管理技术(这实际上意味着 reference counting, at least when you consider memory resources and ignore the other ones such as files) and genuine garbage collection techniques is the handling of circular references (for cyclic graphs)之间的主要区别。
使用引用计数,您需要专门为它们编写代码(使用 weak references 或其他东西)。
在许多有用的情况下(想想 std::vector<std::map<std::string,int>>
)引用计数是隐式的(因为它只能是 0 或 1)并且实际上被省略了,但是构造函数和析构函数(对 RAII 必不可少)表现好像有一个引用计数位(实际上不存在)。在 std::shared_ptr
中有一个真正的参考计数器。但是内存仍然是 隐式 manually managed (new
和 delete
在构造函数和析构函数中触发),但是 "implicit" delete
(在析构函数中)给人一种自动内存管理的错觉。但是,对 new
和 delete
的调用仍然会发生(并且它们会花费时间)。
顺便说一句,GC 实现 可能(并且经常)以某种特殊方式处理循环,但是你把这个负担留给了 GC(例如阅读 Cheney's algorithm ).
一些 GC 算法(特别是分代复制垃圾收集器)不会为 单个 对象释放内存,而是释放 整体 复制后。在实践中,Ocaml GC(或 SBCL)可以比真正的 C++ RAII 编程风格更快(对于 一些,而不是全部,一种算法)。
一些 GC 提供 finalization(主要用于管理 非内存 文件等外部资源),但您很少会使用它(因为大多数值只消耗内存资源)。缺点是终结不提供任何时间保证。实际上,使用终结的程序将其用作最后的手段(例如,文件的关闭仍应或多或少地明确地发生在终结之外,并且与它们一起发生)。
您仍然可以使用 GC(以及使用 RAII,至少在使用不当时)发生内存泄漏,例如当一个值保存在某个变量或某个字段中但将来永远不会使用时。它们发生的频率较低。
我建议阅读 garbage collection handbook。
在您的 C++ 代码中,您可以使用 Boehm's GC or Ravenbrook's MPS or code your own tracing garbage collector。当然使用 GC 是一种权衡(存在一些不便,例如非确定性、缺乏时间保证等...)。
我不认为 RAII 是所有情况下处理内存的最终方式。在某些情况下,在真正有效的 GC 实现(想想 Ocaml 或 SBCL)中编写程序比在 C++17 中使用花哨的 RAII 样式编写代码更简单(开发)和更快(执行)。在其他情况下则不然。 YMMV.
举个例子,如果你用最奇特的 RAII 风格在 C++17 中编写一个 Scheme 解释器,你仍然需要在里面编写(或使用)一个 explicit GC它(因为 Scheme 堆有循环)。大多数 proof assistants are coded in GC-ed languages, often functional ones, (the only one I know which is coded in C++ is Lean) 都是有充分理由的。
顺便说一句,我有兴趣找到这样一个 Scheme 的 C++17 实现(但对自己编码不太感兴趣),最好具有一些多线程能力。
"Efficient" 是一个非常宽泛的术语,从开发工作的角度来看,RAII 通常比 GC 效率低,但在性能方面,GC 通常比 RAII 效率低。然而,可以为这两种情况提供对比示例。当您在托管语言中拥有非常清晰的资源(取消)分配模式时,处理通用 GC 可能会相当麻烦,就像当 shared_ptr
无缘无故地用于所有内容时,使用 RAII 的代码可能效率低得惊人。
If I strictly follow RAII rules, which seems to be a good thing, why would that be any different from having a garbage collector in C++?
虽然两者都处理分配,但它们以完全不同的方式进行。如果您正在引用像 Java 中那样的 GC,那会增加自己的开销,从资源释放过程中移除一些确定性并处理循环引用。
您可以针对特定情况实施 GC,但性能特征大不相同。我在 high-performance/high-throughput 服务器中实现了一次关闭套接字连接(仅调用套接字关闭 API 花费了太长时间并且降低了吞吐量性能)。这不涉及内存,但涉及网络连接,并且不涉及循环依赖性处理。
I know that with RAII the programmer is in full control of when the resources are freed again, but is that in any case beneficial to just having a garbage collector?
这种确定性是 GC 根本不允许的功能。有时您希望能够知道在某个时间点之后执行了清理操作(删除临时文件、关闭网络连接等)。
在这种情况下,GC 不会削减它,这就是 C# 中的原因(例如)你有 IDisposable
接口。
I even heard that having a garbage collector can be more efficient, as it can free larger chunks of memory at a time instead of freeing small memory pieces all over the code.
可以...取决于实现。
垃圾收集解决了 RAII 无法解决的某些 类 资源问题。基本上,它归结为循环依赖,您事先没有确定循环。
这给了它两个优势。首先,RAII 无法解决某些类型的问题。根据我的经验,这些很少见。
更重要的是,它让程序员变得懒惰,不关心内存资源生命周期和某些您不介意延迟清理的其他资源。当您不必关心某些类型的问题时,您可以关心 更多 其他问题。这让您可以专注于您想要关注的问题部分。
缺点是没有 RAII,很难管理您想要限制其生命周期的资源。 GC 语言基本上将您简化为具有极其简单的范围绑定生命周期,或者要求您手动进行资源管理,就像在 C 中一样,手动声明您已完成资源。他们的对象生命周期系统与 GC 紧密相关,并且不能很好地用于大型复杂(但无循环)系统的严格生命周期管理。
公平地说,C++ 中的资源管理需要大量工作才能在如此庞大的复杂(但无循环)系统中正确完成。 C# 和类似的语言只是让它变得更难,作为交换,它们使简单的情况变得简单。
大多数 GC 实现还强制非本地完全成熟 类;创建通用对象的连续缓冲区,或将通用对象组合成一个更大的对象,对于大多数 GC 实现来说并不容易。另一方面,C# 允许您创建值类型 struct
s,但功能有些有限。在现在CPU架构时代,缓存友好性是关键,locality GC力量的缺乏是一个沉重的负担。由于这些语言大部分都具有字节码运行时,理论上 JIT 环境可以将常用数据移动到一起,但与 C++ 相比,由于频繁的缓存未命中,您往往会得到统一的性能损失。
GC 的最后一个问题是释放是不确定的,有时会导致性能问题。与过去相比,现代 GC 减少了这个问题。
RAII 统一处理任何可描述为资源的东西。动态分配就是这样一种资源,但它们绝不是唯一的一种,而且可以说 不是 最重要的一种。文件、套接字、数据库连接、GUI 反馈等等都可以使用 RAII 进行确定性管理。
GC只处理动态分配,让程序员不用担心程序生命周期内分配对象的总量(他们只需要关心峰值并发分配量拟合)
RAII和GC解决问题的方向完全不同。他们是完全不同的,不管有些人怎么说。
两者都解决了资源管理困难的问题。垃圾收集解决了这个问题,使开发人员无需过多关注管理这些资源。 RAII 通过让开发人员更容易关注他们的资源管理来解决这个问题。任何说他们做同样事情的人都有东西要卖给你。
如果您查看最近的语言趋势,就会发现两种方法都在同一种语言中使用,因为坦率地说,您确实需要解决难题的两面。您会看到许多使用各种垃圾收集的语言,这样您就不必关注大多数对象,并且这些语言还提供 RAII 解决方案(例如 python 的 with
运算符) 在你真正想要关注它们的时候。
- C++ 通过 constructors/destructors 提供 RAII,通过
shared_ptr
提供 GC(如果我可以论证引用计数和 GC 在相同的 class 解决方案中,因为它们都是设计的帮助您无需关注寿命)
- Python 通过
with
提供 RAII,通过引用计数系统和垃圾收集器提供 GC
- C# 通过
IDisposable
和 using
提供 RAII,并通过分代垃圾收集器提供 GC
每种语言都出现了这种模式。
关于一个或另一个是 "beneficial" 还是更多 "efficient" 的问题的主要部分,如果不提供大量上下文和争论这些术语的定义,就无法回答。
除此之外,大家基本都能在评论中感受到古代"Is Java or C++ the better language?" 口水战噼啪作响的紧张气氛。我想知道这个问题的 "acceptable" 答案会是什么样子,并且很想最终看到它。
但是关于可能重要的概念差异的一点尚未指出:使用 RAII,您被绑定到调用析构函数的线程。如果您的应用程序是单线程的(即使是 Herb Sutter 指出 The Free Lunch Is Over:当今大多数软件实际上仍然是 是 单线程的),那么单核可能是忙于处理与实际程序不再相关的对象的清理...
与此相反,垃圾收集器通常在自己的线程中运行,甚至是多个线程,因此(在某种程度上)与其他部分的执行解耦。
(注意:一些答案已经尝试指出具有不同特征的应用程序模式,提到了效率、性能、延迟和吞吐量——但尚未提到这一点)
垃圾收集和 RAII 各自支持一种通用构造,而另一种并不适合。
在垃圾收集系统中,代码可以有效地将对不可变对象(例如字符串)的引用视为其中包含的数据的代理;传递此类引用几乎与传递 "dumb" 指针一样便宜,并且比为每个所有者制作单独的数据副本或试图跟踪数据共享副本的所有权更快。此外,垃圾收集系统通过编写一个 class 来创建一个可变对象,根据需要填充它,并提供访问器方法,从而使创建不可变对象类型变得容易,同时避免泄漏对任何可能变异的引用一旦构造函数完成。在需要广泛复制对不可变对象的引用但对象本身不需要的情况下,GC 轻而易举地击败了 RAII。
另一方面,RAII 非常擅长处理对象需要从外部实体获取独占服务的情况。虽然许多 GC 系统允许对象定义 "Finalize" 方法并在发现它们被放弃时请求通知,并且这些方法有时可能会设法释放不再需要的外部服务,但它们很少可靠到足以提供令人满意的确保及时发布外部服务的方法。对于不可替代的外部资源的管理,RAII 轻而易举地击败了 GC。
GC 获胜与 RAII 获胜的情况之间的主要区别在于,GC 擅长管理可按需释放的可替代内存,但不擅长处理不可替代资源。 RAII 擅长处理拥有明确所有权的对象,但不擅长处理无主的不可变数据持有者,除了它们包含的数据之外没有真实身份。
因为 GC 和 RAII 都不能很好地处理所有场景,所以语言为它们提供良好的支持会很有帮助。不幸的是,专注于一个的语言往往将另一个作为事后的想法。
RAII 和垃圾收集旨在解决不同的问题。
当您使用 RAII 时,您会在堆栈上留下一个对象,其唯一目的是在离开方法的范围时清理您想要管理的任何对象(套接字、内存、文件等)。这是为了 异常安全 ,而不仅仅是垃圾收集,这就是为什么您会收到有关关闭套接字和释放互斥量等的响应。 (好吧,所以除了我没有人提到互斥。)如果抛出异常,堆栈展开自然会清理方法使用的资源。
垃圾收集是内存的程序化管理,但如果您愿意,您可以 "garbage-collect" 其他稀缺资源。在 99% 的情况下,明确释放它们更有意义。将 RAII 用于文件或套接字之类的唯一原因是您希望在方法 returns.
时完成资源的使用
垃圾收集还处理 堆分配 的对象,例如当工厂构造一个对象的实例并 return 时。在控制必须离开范围的情况下拥有持久对象是垃圾收集具有吸引力的原因。但是您可以在工厂中使用 RAII,这样如果在您之前抛出异常 return,您就不会泄漏资源。
I even heard that having a garbage collector can be more efficient, as it can free larger chunks of memory at a time instead of freeing small memory pieces all over the code.
使用 RAII(或使用普通的 malloc/free),这是完全可行的 - 事实上,实际上已经完成了。你看,你不一定总是使用默认分配器,它只零碎地释放。在某些情况下,您使用具有不同类型功能的自定义分配器。一些分配器具有一次性释放某个分配器区域中所有内容的内置功能,而无需迭代各个分配的元素。
当然,您随后会遇到何时取消分配所有内容的问题 - 是否必须对这些分配器(或与它们相关联的内存板)的使用进行 RAII 处理,以及如何进行处理。
我最近在 CppCon 2016 上观看了 Herb Sutter 关于 "Leak Free C++..." 的精彩演讲,他谈到了使用智能指针来实现 RAII(资源获取即初始化)——概念以及它们如何解决大部分内存泄漏问题。
现在我在想。如果我严格遵守 RAII 规则,这似乎是一件好事,为什么这与在 C++ 中使用垃圾收集器有什么不同?我知道使用 RAII 时,程序员可以完全控制何时再次释放资源,但是在任何情况下,仅拥有垃圾收集器是否有益?真的会效率低吗?我什至听说有一个垃圾收集器可以更有效,因为它可以一次释放更大的内存块,而不是在整个代码中释放小块内存。
垃圾收集器的一个问题是很难预测程序性能。
使用 RAII,您知道在准确的时间资源将超出范围,您将清除一些内存,这需要一些时间。但是,如果您不精通垃圾收集器设置,则无法预测何时会进行清理。
例如:使用 GC 可以更有效地清理一堆小对象,因为它可以释放大块,但操作速度不快,而且很难预测何时会发生 in,因为 "large chunk cleanup" 这将占用一些处理器时间并会影响您的程序性能。
粗略地说。对于 latency 和 jitter,RAII 惯用法可能更好。对于系统的 吞吐量 .
,垃圾收集器可能更好请注意,RAII is a programming idiom, while GC 是一种内存管理技术。所以我们正在比较苹果和橙子。
但我们可以将 RAII 限制在其内存管理方面仅并将其与 GC 技术进行比较。
所谓的基于 RAII 的内存管理技术(这实际上意味着 reference counting, at least when you consider memory resources and ignore the other ones such as files) and genuine garbage collection techniques is the handling of circular references (for cyclic graphs)之间的主要区别。
使用引用计数,您需要专门为它们编写代码(使用 weak references 或其他东西)。
在许多有用的情况下(想想 std::vector<std::map<std::string,int>>
)引用计数是隐式的(因为它只能是 0 或 1)并且实际上被省略了,但是构造函数和析构函数(对 RAII 必不可少)表现好像有一个引用计数位(实际上不存在)。在 std::shared_ptr
中有一个真正的参考计数器。但是内存仍然是 隐式 manually managed (new
和 delete
在构造函数和析构函数中触发),但是 "implicit" delete
(在析构函数中)给人一种自动内存管理的错觉。但是,对 new
和 delete
的调用仍然会发生(并且它们会花费时间)。
顺便说一句,GC 实现 可能(并且经常)以某种特殊方式处理循环,但是你把这个负担留给了 GC(例如阅读 Cheney's algorithm ).
一些 GC 算法(特别是分代复制垃圾收集器)不会为 单个 对象释放内存,而是释放 整体 复制后。在实践中,Ocaml GC(或 SBCL)可以比真正的 C++ RAII 编程风格更快(对于 一些,而不是全部,一种算法)。
一些 GC 提供 finalization(主要用于管理 非内存 文件等外部资源),但您很少会使用它(因为大多数值只消耗内存资源)。缺点是终结不提供任何时间保证。实际上,使用终结的程序将其用作最后的手段(例如,文件的关闭仍应或多或少地明确地发生在终结之外,并且与它们一起发生)。
您仍然可以使用 GC(以及使用 RAII,至少在使用不当时)发生内存泄漏,例如当一个值保存在某个变量或某个字段中但将来永远不会使用时。它们发生的频率较低。
我建议阅读 garbage collection handbook。
在您的 C++ 代码中,您可以使用 Boehm's GC or Ravenbrook's MPS or code your own tracing garbage collector。当然使用 GC 是一种权衡(存在一些不便,例如非确定性、缺乏时间保证等...)。
我不认为 RAII 是所有情况下处理内存的最终方式。在某些情况下,在真正有效的 GC 实现(想想 Ocaml 或 SBCL)中编写程序比在 C++17 中使用花哨的 RAII 样式编写代码更简单(开发)和更快(执行)。在其他情况下则不然。 YMMV.
举个例子,如果你用最奇特的 RAII 风格在 C++17 中编写一个 Scheme 解释器,你仍然需要在里面编写(或使用)一个 explicit GC它(因为 Scheme 堆有循环)。大多数 proof assistants are coded in GC-ed languages, often functional ones, (the only one I know which is coded in C++ is Lean) 都是有充分理由的。
顺便说一句,我有兴趣找到这样一个 Scheme 的 C++17 实现(但对自己编码不太感兴趣),最好具有一些多线程能力。
"Efficient" 是一个非常宽泛的术语,从开发工作的角度来看,RAII 通常比 GC 效率低,但在性能方面,GC 通常比 RAII 效率低。然而,可以为这两种情况提供对比示例。当您在托管语言中拥有非常清晰的资源(取消)分配模式时,处理通用 GC 可能会相当麻烦,就像当 shared_ptr
无缘无故地用于所有内容时,使用 RAII 的代码可能效率低得惊人。
If I strictly follow RAII rules, which seems to be a good thing, why would that be any different from having a garbage collector in C++?
虽然两者都处理分配,但它们以完全不同的方式进行。如果您正在引用像 Java 中那样的 GC,那会增加自己的开销,从资源释放过程中移除一些确定性并处理循环引用。
您可以针对特定情况实施 GC,但性能特征大不相同。我在 high-performance/high-throughput 服务器中实现了一次关闭套接字连接(仅调用套接字关闭 API 花费了太长时间并且降低了吞吐量性能)。这不涉及内存,但涉及网络连接,并且不涉及循环依赖性处理。
I know that with RAII the programmer is in full control of when the resources are freed again, but is that in any case beneficial to just having a garbage collector?
这种确定性是 GC 根本不允许的功能。有时您希望能够知道在某个时间点之后执行了清理操作(删除临时文件、关闭网络连接等)。
在这种情况下,GC 不会削减它,这就是 C# 中的原因(例如)你有 IDisposable
接口。
I even heard that having a garbage collector can be more efficient, as it can free larger chunks of memory at a time instead of freeing small memory pieces all over the code.
可以...取决于实现。
垃圾收集解决了 RAII 无法解决的某些 类 资源问题。基本上,它归结为循环依赖,您事先没有确定循环。
这给了它两个优势。首先,RAII 无法解决某些类型的问题。根据我的经验,这些很少见。
更重要的是,它让程序员变得懒惰,不关心内存资源生命周期和某些您不介意延迟清理的其他资源。当您不必关心某些类型的问题时,您可以关心 更多 其他问题。这让您可以专注于您想要关注的问题部分。
缺点是没有 RAII,很难管理您想要限制其生命周期的资源。 GC 语言基本上将您简化为具有极其简单的范围绑定生命周期,或者要求您手动进行资源管理,就像在 C 中一样,手动声明您已完成资源。他们的对象生命周期系统与 GC 紧密相关,并且不能很好地用于大型复杂(但无循环)系统的严格生命周期管理。
公平地说,C++ 中的资源管理需要大量工作才能在如此庞大的复杂(但无循环)系统中正确完成。 C# 和类似的语言只是让它变得更难,作为交换,它们使简单的情况变得简单。
大多数 GC 实现还强制非本地完全成熟 类;创建通用对象的连续缓冲区,或将通用对象组合成一个更大的对象,对于大多数 GC 实现来说并不容易。另一方面,C# 允许您创建值类型 struct
s,但功能有些有限。在现在CPU架构时代,缓存友好性是关键,locality GC力量的缺乏是一个沉重的负担。由于这些语言大部分都具有字节码运行时,理论上 JIT 环境可以将常用数据移动到一起,但与 C++ 相比,由于频繁的缓存未命中,您往往会得到统一的性能损失。
GC 的最后一个问题是释放是不确定的,有时会导致性能问题。与过去相比,现代 GC 减少了这个问题。
RAII 统一处理任何可描述为资源的东西。动态分配就是这样一种资源,但它们绝不是唯一的一种,而且可以说 不是 最重要的一种。文件、套接字、数据库连接、GUI 反馈等等都可以使用 RAII 进行确定性管理。
GC只处理动态分配,让程序员不用担心程序生命周期内分配对象的总量(他们只需要关心峰值并发分配量拟合)
RAII和GC解决问题的方向完全不同。他们是完全不同的,不管有些人怎么说。
两者都解决了资源管理困难的问题。垃圾收集解决了这个问题,使开发人员无需过多关注管理这些资源。 RAII 通过让开发人员更容易关注他们的资源管理来解决这个问题。任何说他们做同样事情的人都有东西要卖给你。
如果您查看最近的语言趋势,就会发现两种方法都在同一种语言中使用,因为坦率地说,您确实需要解决难题的两面。您会看到许多使用各种垃圾收集的语言,这样您就不必关注大多数对象,并且这些语言还提供 RAII 解决方案(例如 python 的 with
运算符) 在你真正想要关注它们的时候。
- C++ 通过 constructors/destructors 提供 RAII,通过
shared_ptr
提供 GC(如果我可以论证引用计数和 GC 在相同的 class 解决方案中,因为它们都是设计的帮助您无需关注寿命) - Python 通过
with
提供 RAII,通过引用计数系统和垃圾收集器提供 GC - C# 通过
IDisposable
和using
提供 RAII,并通过分代垃圾收集器提供 GC
每种语言都出现了这种模式。
关于一个或另一个是 "beneficial" 还是更多 "efficient" 的问题的主要部分,如果不提供大量上下文和争论这些术语的定义,就无法回答。
除此之外,大家基本都能在评论中感受到古代"Is Java or C++ the better language?" 口水战噼啪作响的紧张气氛。我想知道这个问题的 "acceptable" 答案会是什么样子,并且很想最终看到它。
但是关于可能重要的概念差异的一点尚未指出:使用 RAII,您被绑定到调用析构函数的线程。如果您的应用程序是单线程的(即使是 Herb Sutter 指出 The Free Lunch Is Over:当今大多数软件实际上仍然是 是 单线程的),那么单核可能是忙于处理与实际程序不再相关的对象的清理...
与此相反,垃圾收集器通常在自己的线程中运行,甚至是多个线程,因此(在某种程度上)与其他部分的执行解耦。
(注意:一些答案已经尝试指出具有不同特征的应用程序模式,提到了效率、性能、延迟和吞吐量——但尚未提到这一点)
垃圾收集和 RAII 各自支持一种通用构造,而另一种并不适合。
在垃圾收集系统中,代码可以有效地将对不可变对象(例如字符串)的引用视为其中包含的数据的代理;传递此类引用几乎与传递 "dumb" 指针一样便宜,并且比为每个所有者制作单独的数据副本或试图跟踪数据共享副本的所有权更快。此外,垃圾收集系统通过编写一个 class 来创建一个可变对象,根据需要填充它,并提供访问器方法,从而使创建不可变对象类型变得容易,同时避免泄漏对任何可能变异的引用一旦构造函数完成。在需要广泛复制对不可变对象的引用但对象本身不需要的情况下,GC 轻而易举地击败了 RAII。
另一方面,RAII 非常擅长处理对象需要从外部实体获取独占服务的情况。虽然许多 GC 系统允许对象定义 "Finalize" 方法并在发现它们被放弃时请求通知,并且这些方法有时可能会设法释放不再需要的外部服务,但它们很少可靠到足以提供令人满意的确保及时发布外部服务的方法。对于不可替代的外部资源的管理,RAII 轻而易举地击败了 GC。
GC 获胜与 RAII 获胜的情况之间的主要区别在于,GC 擅长管理可按需释放的可替代内存,但不擅长处理不可替代资源。 RAII 擅长处理拥有明确所有权的对象,但不擅长处理无主的不可变数据持有者,除了它们包含的数据之外没有真实身份。
因为 GC 和 RAII 都不能很好地处理所有场景,所以语言为它们提供良好的支持会很有帮助。不幸的是,专注于一个的语言往往将另一个作为事后的想法。
RAII 和垃圾收集旨在解决不同的问题。
当您使用 RAII 时,您会在堆栈上留下一个对象,其唯一目的是在离开方法的范围时清理您想要管理的任何对象(套接字、内存、文件等)。这是为了 异常安全 ,而不仅仅是垃圾收集,这就是为什么您会收到有关关闭套接字和释放互斥量等的响应。 (好吧,所以除了我没有人提到互斥。)如果抛出异常,堆栈展开自然会清理方法使用的资源。
垃圾收集是内存的程序化管理,但如果您愿意,您可以 "garbage-collect" 其他稀缺资源。在 99% 的情况下,明确释放它们更有意义。将 RAII 用于文件或套接字之类的唯一原因是您希望在方法 returns.
时完成资源的使用垃圾收集还处理 堆分配 的对象,例如当工厂构造一个对象的实例并 return 时。在控制必须离开范围的情况下拥有持久对象是垃圾收集具有吸引力的原因。但是您可以在工厂中使用 RAII,这样如果在您之前抛出异常 return,您就不会泄漏资源。
I even heard that having a garbage collector can be more efficient, as it can free larger chunks of memory at a time instead of freeing small memory pieces all over the code.
使用 RAII(或使用普通的 malloc/free),这是完全可行的 - 事实上,实际上已经完成了。你看,你不一定总是使用默认分配器,它只零碎地释放。在某些情况下,您使用具有不同类型功能的自定义分配器。一些分配器具有一次性释放某个分配器区域中所有内容的内置功能,而无需迭代各个分配的元素。
当然,您随后会遇到何时取消分配所有内容的问题 - 是否必须对这些分配器(或与它们相关联的内存板)的使用进行 RAII 处理,以及如何进行处理。