一、所有权: 单一/共享

对比单一/共享所有权

单一/共享所有权对比

单一所有权:掌控生杀大权

从多引用开始

多重堆引用问题

多重堆引用的问题

Rust如何解决

方案一、单一所有权

单一所有权解决多重引用问题

rust所有权规则解决多重引用问题

方案二、Copy

使用Copy解决多重引用问题

Copy解决

单一所有权规则整理

单一所有权规则整理

所有权规则整理

单一所有权借用

两种传参方式:传值/传址

传值 or 传址

传值/传址

只读借用/引用

只读借用/引用

只读

借用的生命周期与约束

三段生命周期分析

三段生命周期分析

可变借用/引用

同一个上下文中多个可变引用是不安全的,那如果同时有一个可变引用和若干个只读引 用就可以

第一性原理理解单一所有权规则

第一性原理理解所有权模型:单一所有权/共享所有权

第一性原理

共享内存-多个所有者:引用计数

单一所有权与多个所有者是否有冲突?

  1. 静态检查:单一所有权是rust的编译期默认检查内容
  2. 动态检查:多个所有者主要是用于共享内存,这是专门提供Rc/Arc、Box::leak()、RefCell/Mutex/RwLock等工具。

这里其实可以看出rust如何使用’二八法则’解决问题:

  • 对于常用场景,用编译期静态检查来默认解决
  • 对于特别场景,用专门的语法显式表达出来,提供运行期动态检查来专门解决

Rc使用说明: 只读引用计数

对一个 Rc 结构进行 clone(),不会将其内部的数据复制,只会增加引用计数

use std::rc::Rc;
fn main() {
    let a = Rc::new(1);
    let b = a.clone();
    let c = a.clone();
}

上方代码Rc引用计数示意图:共享堆内存

引用计数示意图

clone源码

fn clone(&self) -> Rc<T> {
    // 增加引用计数
    self.inner().inc_strong();
    // 通过 self.ptr 生成一个新的 Rc 结构
    Self::from_inner(self.ptr)
}

Rc使用Box::leak()

使用Box::leak()创建不受栈内存控制堆堆内存示意图

使用Box::leak()创建不受栈内存控制堆堆内存示意图

有了 Box::leak(),我们就可以跳出 Rust 编译器的静态检查

保证 Rc 指向的堆内存,有最大的生命周期,然后我们再通过引用计数,在合适的时机,结束这段内存的生命周期。如果你对此感兴趣,可以看 Rc::new() 的源码

使用Rc实现DAG

DAG数据结构示意图

DAG数据结构

不可修改版本

use std::rc::Rc;

#[allow(dead_code)]
#[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().cloned()
    }
}

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);

    // 无法编译通过: cannot borrow as mutable
    // let node5 = Node::new(5);
    // let node3 = node1.get_downstream().unwrap();
    // node3.update_downstream(Rc::new(node5));

    // println!("node1: {:?}, node2: {:?}", node1, node2);
}
  • new():建立一个新的 Node。
  • update_downstream():设置 Node 的 downstream。
  • get_downstream():clone 一份 Node 里的 downstream。

RefCell: 提供内部可变性,可变引用计数

外部可变性与内部可变性

外部可变性与内部可变性对比图

外部可变性与内部可变性

RefCell内部可变性示意图

使用RefCell实现可修改版本DAG

RefCell简单使用

获得 RefCell 内部数据的可变借用

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(1);
    {
        // 获得 RefCell 内部数据的可变借用
        let mut v = data.borrow_mut();
        *v += 1;
    }
    println!("data: {:?}", data.borrow());
}

使用RefCell实现可修改版本DAG

use std::cell::RefCell;
use std::rc::Rc;

#[allow(dead_code)]
#[derive(Debug)]
struct Node {
    id: usize,
    // 使用 Rc<RefCell<T>> 让节点可以被修改
    downstream: Option<Rc<RefCell<Node>>>,
}

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

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

    pub fn get_downstream(&self) -> Option<Rc<RefCell<Node>>> {
        self.downstream.as_ref().cloned()
    }
}

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(RefCell::new(node4)));
    node1.update_downstream(Rc::new(RefCell::new(node3)));
    node2.update_downstream(node1.get_downstream().unwrap());
    println!("node1: {:?}, node2: {:?}", node1, node2);

    let node5 = Node::new(5);
    let node3 = node1.get_downstream().unwrap();
    // 获得可变引用,来修改 downstream
    node3.borrow_mut().downstream = Some(Rc::new(RefCell::new(node5)));

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

  1. 首先数据结构的 downstream 需要 Rc 内部嵌套一个 RefCell
  2. 这样,就可以利用 RefCell 的内部可变性,来获得数据的可变借用
  3. 同时 Rc 还允许值有多个所有者。

线程安全版本计数器:Arc(Rc)、Mutex/RwLock(RefCell)

Rust实现两套不同的引用计数数据结构

Arc 内部的引用计数使用了 Atomic Usize ,而非普通的 usize。 从名称上也可以感觉出来,Atomic Usize 是 usize 的原子类型,它使用了 CPU 的特殊指令,来保证多线程下的安全。 如果你对原子类型感兴趣,可以看 std::sync::atomic 的文档。

Rust 实现两套不同的引用计数数据结构,完全是为了性能考虑,从这里我们也可以感受到 Rust 对性能的极致渴求:

  • 如果不用跨线程访问,可以用效率非常高的 Rc; 如果要跨线程访问,那么必须用 Arc。
  • 同样的,RefCell 也不是线程安全的,如果我们要在多线程中,使用内部可变性,Rust 提供了 Mutex 和 RwLock。

Mutex/RwLock其实是并发的两个方案

这两个数据结构你应该都不陌生:

  1. Mutex 是互斥量,获得互斥量的线程对数据独占访问.
  2. RwLock 是读写锁,获得写锁的线程对数据独占访问,但当没有写锁的时候,允许有多个读锁。
  3. 读写锁的规则和 Rust 的借用规则非常类似,我们可以类比着学。
  4. Mutex 和 RwLock 都用在多线程环境下,对共享数据访问的保护上。
  5. 前面构建的 DAG 如果要用在多线程环境下,需要把 Rc> 替换为 Arc> 或者 Arc>。