【Rust 中级教程】 12 共享所有权

360 阅读5分钟

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> 和 PhantomDataPhantomData 这个空结构体很有趣,字面意识是“虚幻数据”,它不占内存空间,主要用于帮助编译器做检查。另外一个就是 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 的地址0x0000007dbf6ff810c 的地址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。