09|所有权:一个值可以有多个所有者么?

190 阅读6分钟

正式开始

运行时动态检查

使用引用计数的智能指针:Rc(Reference counter) 和 Arc(Atomic reference counter)

RC

  1. 对某个数据结构 T,创建引用计数 Rc,使其有多个所有者。Rc 会把对应的数据结构创建在堆上
  2. 如果想对数据创建更多的所有者,通过 clone() 来完成
  3. 对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数。而当一个 Rc 结构离开作用域被 drop() 时,也只会减少其引用计数,直到引用计数为零,才会真正清除对应的内存
use std::rc::Rc;
fn main() {
    // a,b,c 指向堆上相同的数据,也就是说,堆上的数据有了三个共享的所有者
    // a 是 Rc::new(1) 的所有者
    let a = Rc::new(1);
    // b 和 c 都调用了 a.clone(),分别得到了一个新的 Rc
    let b = a.clone();
    let c = a.clone();
    // 代码结束时,c 先 drop,引用计数变成 2,
    // 然后 b drop、a drop,引用计数归零,堆上内存被释放
}

image.png

Box::leak() 机制

单一所有权模型:堆内存的生命周期和创建它的栈内存的生命周期保持一致

Box::leak():创建不受栈内存控制的堆内存,从而绕过编译时的所有权规则。

Box

  1. Box 是 Rust 下的智能指针
  2. 它可以强制把任何数据结构创建在堆上
  3. 然后在栈上放一个指针指向这个数据结构
  4. 但此时堆内存的生命周期仍然是受控的,跟栈上的指针一致

Box::leak

  1. 它创建的对象,从堆内存上泄漏出去,不受栈内存控制
  2. 是一个自由的、生命周期可以大到和整个进程的生命周期一致的对象
  3. 在 C/C++ 下,通过 malloc 分配的每一片堆内存,都类似 Rust 下的 Box::leak()

image.png 4. Box::leak()可以跳出 Rust 编译器的静态检查,保证 Rc 指向的堆内存,有最大的生命周期,然后我们再通过引用计数,在合适的时机,结束这段内存的生命周期

所有权的静态检查和动态检查

  1. 静态检查,靠编译器保证代码符合所有权规则
  2. 动态检查,通过 Box::leak 让堆内存拥有不受限的生命周期,然后在运行过程中,通过对引用计数的检查,保证这样的堆内存最终会得到释放。

DAG的实现思路

  1. 假设 Node 就只包含 id 和指向下游(downstream)的指针
  2. DAG 中的一个节点可能被多个其它节点指向,使用 Rc 来表述
  3. 一个节点可能没有下游节点,用 Option> 来表述

image.png


use std::rc::Rc;

#[derive(Debug)]
struct Node {
    id: usize,
    downstream: Option<Rc<Node>>,
}

impl Node {
    pub fn new(id: usize) -> Self {
        Self {
            id,
            downstream: None,
        }
    }

    pub fn update_downstream(&mut self, downstream: Rc<Node>) {
        self.downstream = Some(downstream);
    }

    pub fn get_downstream(&self) -> Option<Rc<Node>> {
        self.downstream.as_ref().map(|v| v.clone())
    }
}

fn main() {
    let mut node1 = Node::new(1);
    let mut node2 = Node::new(2);
    let mut node3 = Node::new(3);
    let node4 = Node::new(4);
    node3.update_downstream(Rc::new(node4));

    node1.update_downstream(Rc::new(node3));
    node2.update_downstream(node1.get_downstream().unwrap());
    println!("node1: {:?}, node2: {:?}", node1, node2);
}

但上面的DAG一旦创建完成之后就无法修改,这是因为 Rc 是一个只读的引用计数器,你无法拿到 Rc 结构内部数据的可变引用,来修改这个数据。为此我们需要使用RefCell

let node5 = Node::new(5);
let node3 = node1.get_downstream().unwrap();
node3.update_downstream(Rc::new(node5));

println!("node1: {:?}, node2: {:?}", node1, node2);

RefCell

RefCell 也绕过了 Rust 编译器的静态检查,允许我们在运行时,对某个只读数据进行可变借用

内部可变性

外部可变性:当我们用 let mut 显式地声明一个可变的值或者用 &mut 声明一个可变引用时,编译器可以在编译时进行严格地检查,保证只有可变的值或者可变的引用,才能修改值内部的数据,这被称作外部可变性(exterior mutability),外部可变性通过 mut 关键字声明

内部可变性:绕开编译时的检查,对并未声明成 mut 的值或者引用进行修改。也就是说,在编译器的眼里,值是只读的,但是在运行时,这个值可以得到可变借用,从而修改内部的数据

use std::cell::RefCell;
fn main() {
    // data 是一个 RefCell,其初始值为 1
    let data = RefCell::new(1);
    // 根据所有权规则,在同一个作用域下,不能同时有活跃的可变借用和不可变借用。
    // 通过这对花括号,明确地缩小了可变借用的生命周期,不至于和后续的不可变借用冲突。
    {
        // 获得 RefCell 内部数据的可变借用
        let mut v = data.borrow_mut();
        // 对它做加 1 的操作
        *v += 1;
    }
    // 通过 RefCell 的 borrow() 方法,获得一个不可变的内部引用
    println!("data: {:?}", data.borrow());
}
小结

image.png

Arc 和 Mutex/RwLock

  1. 多块线程访问同一个内存需要使用 Arc 以及 Mutex/RwLock 来解决

  2. Arc 内部的引用计数使用了 Atomic Usize ,而非普通的 usize

  3. 如果不用跨线程访问,可以用效率非常高的 Rc;如果要跨线程访问,那么必须用 Arc

  4. 如果我们要在多线程中,使用内部可变性,Rust 提供了 Mutex 和 RwLock

    a. Mutex 是互斥量,获得互斥量的线程对数据独占访问

    b. RwLock 是读写锁,获得写锁的线程对数据独占访问,但当没有写锁的时候,允许有多个读锁

    Rc<RefCell<T>> 替换为 Arc<Mutex<T>> 或者 Arc<RwLock<T>>

小结

image.png

好用链接

  1. Clone trait

fn clone(&self) -> Rc<T> {
    // 增加引用计数
    self.inner().inc_strong();
    // 通过 self.ptr 生成一个新的 Rc 结构
    Self::from_inner(self.ptr)
}
  1. Rc::new 源码
  2. Atomic Usize 源码
  3. attomic 源码
  4. thread spawn源码
  5. Arc 源码

新鲜知识点

  1. 最小权限原则
  2. 内部可变性

精选问答

  1. 以下代码注释的部分,运行时不能通过,可变借用和不可变借用并没有冲突呀(v并没在borrow之后使用,同一时刻并没有同时有可变借用和不可变借用)

    fn main() { 
        /*let data = RefCell::new(1); 
           let mut v = data.borrow_mut(); 
           *v += 1; 
           println!("data: {:?}", data.borrow());
        */ 
        let mut v = vec![1, 2, 3]; 
        let data1 = &mut v[1]; 
        *data1 = 2; 
        let data2 = &v[1]; 
        println!("{}", data2); 
    }
    

    a. 这就是运行期检查和编译期检查的区别。

    b. data.borrow_mut() 产生的 v 会一直活跃到作用域结束

    c. 而对于 &mut 编译器可以检查它是否活跃从而让我们撰写代码时不需要额外的作用域

  2. 为啥要week == 0的时候才deallocate所有内存呢? 这样是不是不太高效?

    a. strong 为 0 时,ptr::drop_in_place 已经把大部分内存释放。

    b. 但此时可能还有孤悬的 weak reference,它们还是可用的,但无法 upgrade

    c. weak demo

  3. self.downstream.as_ref().map(|v| v.clone()) 这里的map语法是什么意思?

    a. downstream 是一个 Option

    b. as_ref() 后变成 Option<&Node>

    c. Option 有 map 方法,可以对里面的 Some(v) 的 v 进行 map 处理