rust语言基础学习: 使用智能指针Rc<T>让值可以有多个所有者

rust语言基础学习: 使用智能指针Rc<T>让值可以有多个所有者

2020-07-14
Rust

昨天学习了Rust中的智能指针Box<T>,使用Box可以强制将数据分配在堆上,然后栈上放一个指针指向并拥有这个数据,堆内存中数据的生命周期与栈上指针的生命周期一致。 智能指针Box<T>有三个使用场景:当有大量的数据,并且希望在确保数据不被拷贝的情况下转移所有权的时候;当有一个在编译时未知大小的类型,而又想要在需要确切大小的上下文中使用这个类型值的时候;当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候。

前面我们的学习一直在强调的就是单一所有权,在单一所有权下有了Box<T>,String,Vec<T>等智能指针,就可以利用栈内存的自动管理实现堆内存的自动管理。但在实际中还是会存在单个值需要有多个所有者的使用场景。 今天学习标准库中的另一个智能指针Rc<T>,Rc的使用场景是当数据需要有多个所有者时。

1.一个值有多个所有者的场景 #

我们编写程序的目的是为了解决真实世界的问题,程序要建立真实世界的抽象。考虑有下面的两个实体: 公司(Company)和员工(Employee)。公司实体有个属性: 经理。 有一个名为"张三"的员工实体,有两个公司实体"分公司1"和"分公司2",张三是这两个公司的经理。于是写了下面无法编译通过的代码。

例1:

 1#[derive(Debug)]
 2struct Employee {
 3    name: String,
 4}
 5
 6#[derive(Debug)]
 7struct Company {
 8    name: String,
 9    manager: Employee,
10}
11
12
13
14fn main() {
15    let employee = Employee {
16        name: String::from("张三"),
17    };
18  
19    let company1 = Company {
20        name: String::from("分公司1"),
21        manager: employee, // value moved here
22    };
23    let company2 = Company {
24        name: String::from("分公司2"),
25        manager: employee, // value used here after move
26    };
27   
28    println!("{:?}, {:?}", company1, company2);
29}

上面例1的代码,copmany1coppany2需要同时拥有emplyee。这在编译时是不允许的,Rust的单所有权机制,在21行employee的所有权转移到了company1,所以后边的上下文中employee变量已经失效。这是之前学习的单一所有权规则,是在编译时完成的静态检查,不会影响运行时的效率。 如果根据之前学习的单一所有权和引用生命周期等知识,我们可以将例1的代码修改如下,使其编译通过。

例2:

 1#[derive(Debug)]
 2struct Employee {
 3    name: String,
 4}
 5
 6#[derive(Debug)]
 7struct Company<'a> {
 8    name: String,
 9    manager: &'a Employee,
10}
11
12
13
14fn main() {
15    let employee = &Employee {
16        name: String::from("张三"),
17    };
18  
19    let company1 = Company {
20        name: String::from("分公司1"),
21        manager: employee,
22    };
23    let company2 = Company {
24        name: String::from("分公司2"),
25        manager: employee,
26    };
27   
28    println!("{:?}, {:?}", company1, company2);
29}

例2中Company结构体中的manager属性是Employee引用,标注了其生命周期为'a。这样employee还是单一所有权,company1和company2借用了employee的数据。

但如果我们就是要让Company结构体中的manager属性是Employee,即Company要获得Employee的所有权。这个问题就属于运行时的问题了,因为在运行时才会知道究竟需要创建多少个Company,只有在运行时才会知道Employee作为经理会被多少个Company所有。 前面提到的单一所有权在编译时的静态检查无法处理这种情况,需要在运行时的动态检查(检查还有多少个所有者,好决定是否释放堆内存),当然运行时的动态检查会牺牲一部分效率。Rust标准库里提供的智能指针Rc<T>就可以做这种运行时的动态检查。

2.智能指针Rc的概念和基本用法 #

为了启动多所有权,Rust提供了一个Rc<T>类型的智能指针。其名称为引用计数(reference counting)的缩写,引用计数通过记录着一个值被引用的数量来判断这个值是否还仍被使用,如果某个值有零个引用,就代表没有任何有效引用并可以被清理了。

Rc<T>智能指针的用法如下,对于某个数据类型T可以创建引用计数Rc,使其有多个所有者。Rc会把数据类型T的数据创建在堆上。之后如果想为数据创建更多的所有者,可以通过clone()方法来完成。 注意:

  • 对一个Rc变量进行clone()时,不会将其内部的数据复制,只会增加引用计数。
  • 当一个Rc变量离开作用域被drop()时,只会减少引用计数,直到引用计数为零时,才会真正清除其拥有数据的堆内存。

例3:

 1use std::rc::Rc;
 2
 3fn main() {
 4    let num = Rc::new(5);
 5    println!("{}", Rc::strong_count(&num)); // 1
 6    let num1 = num.clone();
 7    {
 8        println!("{}", Rc::strong_count(&num1)); // 2
 9        let num2 = num.clone();
10        println!("{}", Rc::strong_count(&num2)); // 3
11    }
12    println!("{}", Rc::strong_count(&num)); // 2
13}

例3演示了Rc<T>的基本用法,可以使用Rc::strong_count函数获取引用计数的数量。

使用时需要注意Rc<T>不是线程安全的,它主要用来在同一线程上共享T的所有权。 Rc<T>的内存布局示意图如下:

rust-rc-layout.png

例3中的代码执行到第8行时,内存示意图如下:

rust-rc-layout-demo1.png

从内存示意图可以看出,当调用num.clone()方法时,只是复制了Rc在栈内存上结构,在栈上产生一个新的Rc,同时更新堆内存上数据的引用计数。这点也能从Rc源码clone方法实现里看出来。

 1#[stable(feature = "rust1", since = "1.0.0")]
 2impl<T: ?Sized> Clone for Rc<T> {
 3    /// Makes a clone of the `Rc` pointer.
 4    ///
 5    /// This creates another pointer to the same allocation, increasing the
 6    /// strong reference count.
 7    ///
 8    /// # Examples
 9    ///
10    /// ```
11    /// use std::rc::Rc;
12    ///
13    /// let five = Rc::new(5);
14    ///
15    /// let _ = Rc::clone(&five);
16    /// ```
17    #[inline]
18    fn clone(&self) -> Rc<T> {
19        self.inner().inc_strong();
20        Self::from_inner(self.ptr)
21    }
22}

学到了这里,就产生了一个矛盾,根据前面学习的所有权规则,一个值在同一时刻只能有一个所有者,所有权规则是由编译器保证的,那么Rc<T>又是如何跳过编译器的检查,在同一线程上共享T的所有权呢。这个属于Rust高级知识,目前我们学习Rust基础只需要了解以下内容就行,从Rc::new的实现来看:

 1 #[cfg(not(no_global_oom_handling))]
 2    #[stable(feature = "rust1", since = "1.0.0")]
 3    pub fn new(value: T) -> Rc<T> {
 4        // There is an implicit weak pointer owned by all the strong
 5        // pointers, which ensures that the weak destructor never frees
 6        // the allocation while the strong destructor is running, even
 7        // if the weak pointer is stored inside the strong one.
 8        Self::from_inner(
 9            Box::leak(box RcBox { strong: Cell::new(1), weak: Cell::new(1), value }).into(),
10        )
11    }

上面Rc::new()的源码汇总使用了Box::leak(),leak是泄露的意思,Box::leak表示创建不受栈内存控制的堆内存,这样就能绕过编译器的所有权规则检查。即上面内存示意图中堆内存中的数据是泄露出来的,在编译时会跳过所有权规则的静态检查,泄露的堆内存会在运行时根据引用计数,在合适的时间被释放。

3.使用Rc的示例 #

学习了智能指针Rc<T>的概念和基本用法,下面我们在例1中的无法编译的代码中使用Rc,使其能编译通过,实现一个值有多个所有者的功能。

 1
 2#[derive(Debug)]
 3struct Employee {
 4    name: String,
 5}
 6
 7#[derive(Debug)]
 8struct Company {
 9    name: String,
10    manager: Rc<Employee>,
11}
12
13
14
15use std::rc::Rc;
16
17fn main() {
18    let employee = Rc::new(Employee {
19        name: String::from("张三"),
20    });
21  
22    let company1 = Company {
23        name: String::from("分公司1"),
24        manager: employee.clone(),
25    };
26    let company2 = Company {
27        name: String::from("分公司2"),
28        manager: employee,
29    };
30   
31    println!("{:?}, {:?}", company1, company2);
32  
33
34}

4.总结 #

本节我们学习了智能指针Rc<T>。了解了在需要使一个值有多个所有者的场景时,需要使用类似Rc这种带有引用计数的智能指针,绕开编译器在编译时对单一所有权的规则检查。 Rc内部使用了Box::leak将分配的堆内存泄露以绕开编译器对所有权规则的检查。到目前为止,我们就学习Rust对所有权的两种检查:

  • 编译时的静态检查: 在编译时由编译器确保我们编写的代码符合所有权规则。
  • 运行时的动态检查: 对于使用Box::leak泄露出来的堆内存将不再受栈内存的自动管理,而是在运行时通过对引用计数的检查,确保在满足条件时堆内存得到释放。

参考 #

© 2024 青蛙小白