陈天 · Rust 编程第一课
陈天
Tubi TV 研发副总裁
新⼈⾸单¥59.9
2733 人已学习
课程目录
已更新 11 讲 / 共 41 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词|让Rust成为你的下一门主力语言
免费
前置篇 (3讲)
01|内存:值放堆上还是放栈上,这是一个问题
02|串讲:编程开发中,那些你需要掌握的基本概念
加餐| Rust真的值得我们花时间学习么?
基础篇 (7讲)
03|初窥门径:从你的第一个Rust程序开始!
04|get hands dirty:来写个实用的CLI小工具
05|get hands dirty:做一个图片服务器有多难?
06|get hands dirty:SQL查询工具怎么一鱼多吃?
07|所有权:值的生杀大权到底在谁手上?
08|所有权:值的借用是如何工作的?
09|所有权:一个值可以有多个所有者么?
陈天 · Rust 编程第一课
15
15
1.0x
00:00/00:00
登录|注册

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

你好,我是陈天。
之前介绍的单一所有权规则,能满足我们大部分场景中分配和使用内存的需求,而且在编译时,通过 Rust 借用检查器就能完成静态检查,不会影响运行时效率。
但是,规则总会有例外,在日常工作中有些特殊情况该怎么处理呢?
一个有向无环图(DAG)中,某个节点可能有两个以上的节点指向它,这个按照所有权模型怎么表述?
多个线程要访问同一块共享内存,怎么办?
我们知道,这些问题在程序运行过程中才会遇到,在编译期,所有权的静态检查无法处理它们,所以为了更好的灵活性,Rust 提供了运行时的动态检查,来满足特殊场景下的需求。
这也是 Rust 处理很多问题的思路:编译时,处理大部分使用场景,保证安全性和效率;运行时,处理无法在编译时处理的场景,会牺牲一部分效率,提高灵活性。后续讲到静态分发和动态分发也会有体现,这个思路很值得我们借鉴。
那具体如何在运行时做动态检查呢?运行时的动态检查又如何与编译时的静态检查自洽呢?
Rust 的答案是使用引用计数的智能指针:Rc(Reference counter) 和 Arc(Atomic reference counter)。这里要特别说明一下,Arc 和 ObjC/Swift 里的 ARC(Automatic Reference Counting)不是一个意思,不过它们解决问题的手段类似,都是通过引用计数完成的。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/1000字
划线
笔记
复制
该试读文章来自付费专栏《陈天 · Rust 编程第一课》,如需阅读全部文章,
请订阅文章所属专栏新⼈⾸单¥59.9
立即订阅
登录 后留言

精选留言(5)

  • 千回百转无劫山
    1. 错误为线程借用的arr生命周期可能会长于main函数中的arr,简单处理的话把main中arr的所有权move到线程里即可,编译器对此有详尽的提示:
    ```rust
    fn main() {
        let arr = vec![1];

        std::thread::spawn(move || {
            println!("{:?}", arr);
        });
    }
    ```
    2. 这个问题其实是第1个问题的延续,如果将main中变量的所有权move到线程中,那么在main中将无法访问,所以使用Arc这个智能指针即可实现共享所有权:
    ```rust
    use std::sync::Arc;
    fn main() {
        let s = Arc::new("rust rocks!");
        let s1 = s.clone();

        let handler = std::thread::spawn(move || {
            println!("thread: {:?}", s1);
        });
        println!("main: {:?}", s);
        handler.join().unwrap();
    }
    ```
    3. 不太确定,查文档看调用链是inner方法返回了RcBox,RcBox调用的inc_strong是RcInnerPtr这个trait的方法,它会通过调用该trait的strong_ref方法返回cell,而cell是一个可变的共享容器,即最终通过cell共享内存来改变内部数据。

    看完这一小节产生一个疑问,本小节介绍的智能指针都是为了突破rust编译期的所有权规则限制。那为什么要先做限制再提供突破限制的方法呢?这样做的意义是否可以理解为就像unsafe或者最小权限原则一样,大部分情况下,遵循所有权规则,仅在必要的时候使用智能指针来突破限制?如果用户滥用了智能指针,那么是否就像滥用了unsafe一样,rust内存安全等特性就无法保证了?

    作者回复: 赞!非常棒的回答。第 3 题其实不用看代码也可以尝试猜一下:当一个只读引用可以修改内部数据时,它一定是用了内部可变性。:)

    一个实际使用的系统一定是符合二八定律的:可以用少量规则来满足 80%的应用场景。但总还有例外需要处理。所以编译期尽管能解决绝大多数的应用场景,但是解决不了的时候,还是需要运行期的检查来弥补。

    这就涉及到如何权衡了。Rust 的选择是最小权限原则,你只有显式地要求(比如数据结构用 Arc<T> 封装),才会进行额外的处理(维护引用计数)。其实这也是我们撰写软件系统时应该遵循的原则。

    谈到滥用,最小权限原则恰恰是为了防止滥用。比如 mut,当你实际不需要的时候,编译器会报警。Rc/Arc 不必要的 clone,clippy(Rust 的 linting 工具) 会提示。

    如果「滥用」智能指针,并不会导致内存安全无法保证,比如你在不需要的时候使用 Arc,损失的是 atomic compare_and_swap 时的性能,但不会带来安全问题。

    2021-09-10
    2
    2
  • Geek_364411
    1.
    fn main() {
      let arr = vec![1];

      std::thread::spawn(move || {
        println!("{:?}", arr);
      });
    }
    2.
    fn main() {
        let name = "Hello";
        let m_name = Arc::new(name);
        let clone_name = m_name.clone();
        std::thread::spawn(move || {
            println!("{:?}", clone_name);
        });
        println!("{:?}", m_name);
    }
    3.使用了裸指针来修改
    pub fn replace<T>(dest: &mut T, src: T) -> T {
        unsafe {
            let result = ptr::read(dest);
            ptr::write(dest, src);
            result
        }
    }

    作者回复: 1/2 正确!

    3. 其实是想帮助我们回顾内部可变性。当一个不可变的引用可以修改内部状态时,它一定使用了某种内部可变性的方法。你的代码并没有解决 clone() 面临的问题,因为从接口看,你要修改的 dst 是一个 &mut T。clone() 第一个参数是 &self,不是 &mut self。

    2021-09-10
    1
  • noisyes
    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);
    }

    老师这段代码注释的部分,运行时不能通过,可变借用和不可变借用并没有冲突呀(v并没在borrow之后使用,同一时刻并没有同时有可变借用和不可变借用),我自己写的这部分就是可以编译运行的。
    2021-09-10
  • lisiur
    老师您好,在代码3里,把内部作用域去掉会导致运行时错误,但是如果使用普通的借用方式像这样:

    ```rust
    fn main() {
        let mut data = 1;
        let v = &mut data;
        *v += 1;
        println!("data: {:?}", &data);
    }
    ```

    却不需要使用多余的作用域。

    我的理解是普通的借用方式走的是编译期检查,编译器标记借用的生命期的粒度比作用域要小,比如上述代码的 mut 借用,正常的生命期应该是到main函数结束,但是编译器应该是把它缩小到了 println 代码之前的位置,所以 println 的不可变借用并不和上面的可变借用冲突。但是运行时的"生命期检查"应该就是作用域粒度的,所以需要使用额外的作用域来达到手动 drop 可变借用的效果。

    我的想法是,既然编译期能够做到尽可能小的缩小借用的生命周期,那编译器能不能自动对这种特殊的内部可变性的借用在合适的位置插入drop代码,使得不使用额外的作用域也能满足运行时检查呢?
    2021-09-10
  • 葡萄
    3. Rc类创建
    Box::leak(box RcBox { strong: Cell::new(1), weak: Cell::new(1), value })
    self.inner().inc_strong()
    获得了Rc里 strong, 也就是Cell对象,Cell是内部可变的,使用set方法进行修改。
    fn inc_strong(&self) {
            let strong = self.strong();
            self.strong_ref().set(strong + 1);
        }
    2021-09-10
收起评论
5
返回
顶部