专栏名称: 程序人生
十年漫漫程序人生,打过各种杂,也做过让我骄傲的软件;管理过数十人的团队,还带领一班兄弟姐妹创过业,目前在硅谷一家创业公司担任 VP。关注程序人生,了解程序猿,学做程序猿,做好程序猿,让我们的程序人生精彩满满。
目录
相关文章推荐
51好读  ›  专栏  ›  程序人生

透过 Rust 探索系统的本原:内存管理

程序人生  · 公众号  · 程序员  · 2021-04-26 08:05

正文

请到「今天看啥」查看全文


Tracing GC 管理生命周期的方式很巧妙,它并不跟踪每个具体的对象,而是通过扫描当前还有哪些对象仍然在使用,找到那些已经不再使用的对象,释放之。这种方式简单直观高效,但非常粗犷,无法确保某个对象一定在生命周期结束后得到释放。它还有一些副作用,比如消耗额外的 CPU 时间,GC 工作时会打断工作线程的执行,以及需要在内存消耗情况和 GC 频次之间找到一个平衡点等等。Tracing GC 下的内存管理就像我们的大学生活:平时各种浪,考前狂突击。

Swift 采用的是另一种策略 —— ARC(Atomic Reference Counting)。它通过编译时根据每个对象的引用情况,插入相应的引用计数代码(进入函数时,retain - 被引用的对象引用计数增加,退出时 release - 被引用的对象引用计数减少),从而精确掌控每个对象的生命周期:

(图片来自:Swift: Avoiding Memory Leaks by Examples [2])

这种方式的虽然规避了 Tracing GC 的很多副作用,但它为了实时追踪对象的引用情况,在运行时也付出了不少代价:retain/release 做了很多事情,并且为了线程安全,修改引用计数需要加锁或者使用 atomics,这比我想象的要吃性能 [3],并且 ARC 无法处理循环引用 —— 所以需要开发者在适当的地方使用 weak ref 来解除循环引用带来的引用计数问题。

ARC 下的内存管理就像在工厂打工:上班签到,下班打卡,兢兢业业,一丝不苟。

而 Rust,采用了完全不同的方式。对 Rust 而言,像 C/C++ 那样靠开发者手工来追踪对象的生命周期是不靠谱的 —— 人总是会犯错,何况这个世界上合格的程序员非常稀缺;但像 Java/Swift 这样在运行时花费额外代价来追踪生命周期,又和语言本身的目标不符。最终 Rust 沿用和发展了 Cyclone 的生命周期的处理方式。

在我之前的文章《 透过 Rust 探索系统的本原:编程语言 》中,用一幅图概述了 Rust 的所有权和借用规则:

如果对所有权和借用规则不熟悉的读者,可以看我的那篇文章。我们来深入看看,Rust 的所有权和借用规则是如何结合编译期生命周期的推定,来解决堆上内存的生命周期管理的问题的。

我们先用一幅图看 move 是如何处理的:

这段简单的代码里,我们生成了一个 User 对象,然后将其传递给 insert() 函数。在 Rust 里,除非特别声明(使用智能指针),对象(结构)是存储在栈上的。而 Rust 的 String 对象,会在栈上放一个指针,指向堆里的实际字符串数据。由于 Rust 的单一所有权模型,当 user 移动到 insert 函数后, insert 就是其新的 owner,编译器会确保之前的 owner 失去对 user 的访问权:如果继续访问,会得到编译错误。由此,堆上的字符串依旧只有一个 owner(图中红色实线),而旧的 owner 因为被禁止使用,也就间接失去了对堆上数据的引用。当 insert 函数结束时, user 被丢弃(Drop),也即意味着堆上分配的内存被释放(还记得上篇文章讲的 RAII 么)。所以,对于对象的 move,Rust 轻松搞定堆上的数据的生命周期的管理。







请到「今天看啥」查看全文