0x00 开篇
所有权的概念非常苛刻,要求每个值有且仅有一个所有者。但是 Rust 也提供了相应的解决方法——共享所有权和借用。本篇文章将介绍共享所有权的一些概念。本篇文章存在一些还未介绍到的引用概念,如果您对引用概念不是很了解,可以先略过此文章。本篇文章的阅读时间大约 12 分钟。
0x01 引用计数
前面介绍 转移
的概念时,我对比了 python
和 C++
的对赋值概念的内存模型。当时 python
使用的方法是引用计数,其实共享所有权的原理同样是引用计数。Rust 提供了 Rc (Reference Count) 和 Arc (Atomic Reference Count) 两种类型来实现引用计数。
Rc 与 Arc 的用法完全相同,仅仅是应用场景不同,Arc 可以在线程中安全共享,Rc 则并不关注线程安全。接下来的演示我将仅以 Rc 为例。
0x02 Rc 的使用
使用 Rc 可以让一个值拥有多个所有者,每当值增加一个所有者,引用计数就会增加 1。当引用计数为 0 时,变量的内存将会被释放。
Rc 的使用还是很简单的,下面来看下代码吧。
use std::rc::Rc; fn main() { // 创建一个字符串 rust let a: Rc<String> = Rc::new(String::from("rust")); // 调用 clone 方法,使指向字符串的引用计数 +1 let b = a.clone(); // Rc::clone(&a) 等价于 a.clone() let c = Rc::clone(&a); // 输出3个变量指向字符串的地址 println!("{:p}", a.as_ptr()); println!("{:p}", b.as_ptr()); println!("{:p}", c.as_ptr()); // 查看引用计数 println!("引用计数 {} ", Rc::strong_count(&a)); // 下面的代码只做了解 println!("弱引用计数 {} ", Rc::weak_count(&a)); } // 运行结果 // a 的地址: 0x28f97c2ac10 // b 的地址: 0x28f97c2ac10 // c 的地址: 0x28f97c2ac10 // 引用计数 3 // 弱引用计数 0
我们先创建一个拥有共享所有权的字符串 a = "rust"
, 使用克隆方法克隆 a
并与 b
和 c
变量绑定,Rc::clone
和 clone
方法是等价的,使用哪个都可以。最后输出 3 个变量字符串的地址。以及 a
的引用计数。从结果中可以看到地址都是一样的。并且我们还可以直接使用变量 a
来调用 String
的方法(下面是示例代码)。
use std::rc::Rc; fn main() { // 创建一个字符串 rust let a: Rc<String> = Rc::new(String::from("rust")); // 使用 String 中的方法。 if a.contains("ru") { println!("true"); } } // 运行结果 // true
其实 Rc<T>
持有一个指针(接下来的文章会介绍),指向堆空间的 T
值,同时该值还会有一个引用计数。使用 clone
方法只会增加引用计数,并不会复制值。但是使用 Rc 指针引用的值不能被修改。
0x03 了解 Rc 源码
我们再来看下源码。Rc
其实就是一个结构体,有两个字段。其中一个是 NonNull<RcBox>
和 PhantomData
。PhantomData
这个空结构体很有趣,字面意识是“虚幻数据”,它不占内存空间,主要用于帮助编译器做检查。另外一个就是 NonNull
持有一个指针,指向 RcBox
。重点来看 RcBox
, 它有三个字段:strong
就是引用计数了,weak
则就是弱引用计数,value
才是真正的值。
// rc.rs pub struct Rc<T: ?Sized> { ptr: NonNull<RcBox<T>>, phantom: PhantomData<RcBox<T>>, } // rc.rs struct RcBox<T: ?Sized> { strong: Cell<usize>, weak: Cell<usize>, value: T, } // non_null.rs pub struct NonNull<T: ?Sized> { pointer: *const T, } // marker.rs pub struct PhantomData<T: ?Sized>; // rc.rs impl<T: ?Sized> Rc<T> { // ... pub fn strong_count(this: &Self) -> usize { this.inner().strong() } pub fn weak_count(this: &Self) -> usize { this.inner().weak() - 1 } // ... }
PS:有关 Rust 弱引用的相关知识暂不介绍,暂做了解即可。印象中记得,弱引用好像还是一个面试常问的问题,哈哈。
0x04 Rc 的内存布局
我们通过断点再次运行代码,来看下内存。
首先,我们拿到了 a
的地址 0x0000007dbf6ff7f0
, b
的地址0x0000007dbf6ff810
,c
的地址0x0000007dbf6ff810
。可以看到三个变量持有的数据是一样的, 都是0x000001e5c9f7df40
。通过第三小节我们也了解到 Rc
是一个结构体,但是 PhantomData
是一个虚幻数据,不占内存空间,所以这个地址应该指向的就是 RcBox
了。接下来通过这个地址找到数据,验证下我们的猜想。
找到这个地址,红色框8个字节是 strong
字段,黄色框8个字节是 weak
字段,紫色框的24个字段是 value
字段,这里的 value
是 String 类型,String 其实就是封装了向量的结构体,向量的内存布局则分为3部分:数据的指针,长度和容量。如果对向量和字符串内存布局不是很熟悉的读者,可以再回顾下前面的文章。从内存里可以得知 strong
的值是 3,weak
的值是 1。又有读者可能要问了,上面输出的 weak_count
是 0,为什么这里是 1 呢?第三节的源码中可以得知 weak_count
方法返回的值是 weak - 1,所以这里是1。value
的长度是 4,容量也是 4,数据指针是 0x000001e5c9f7be50
,我们再看下,这个地址是不是 rust
呢。
毫无疑问,肯定是的。最后,画个简单的内存布局吧(如下图)。
0x05 小结
本篇文章介绍的是共享所有权,主要了解了 Rc,但是 Rc 有个很明显的特点就是不可修改。Rust 为了安全,设定了既然要共享,那就不可改变这个前提。如果可修改,那就可能出现两个变量互相指向对方,这会导致引用计数永远不会为 0,使得两个值永远都不会释放,最终导致内存泄漏。但是 Rust 也提供了内部修改能力,这部分可能会在高级篇章介绍。文章也提到了弱引用,这个可以先暂时作为了解即可。本文也提到了 Arc,使用方法也大同小异,如果没有多线程的场景,还是建议使用 Rc。