You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

10 KiB

Rc与Arc

Rust所有权机制要求一个值只能有一个所有者在大多数情况下都没有问题但是考虑以下情况:

  • 在图数据结构中,多个边可能会拥有同一个节点,该节点直到没有边指向它时,才应该被释放清理
  • 在多线程中多个线程可能会指向同一个数据但是你受限于Rust的安全机制你又无法同时的可变借用该数据

以上场景不是很常见但是一旦遇到就非常棘手为了解决此类问题Rust在所有权机制之外又引入了额外的措施来简化相应的实现通过引用计数的方式允许一个数据资源在同一时刻拥有多个所有者。

这种实现机制就是RcArc,前者适用于单线程,后者适用于多线程。由于二者大部分时间都是相同,因此本章将以Rc作为讲解主体,对于Arc的不同之处,也将进行单独讲解。

Rc

引用计数(reference counting),顾名思义,通过记录一个数据被引用的次数来确定该数据是否正在被使用。当引用次数归零时,就代表该数据不再被使用,因此可以被清理释放。

Rc正是引用计数的英文缩写。当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用Rc成为数据值的所有者,例如之前提到的多线程场景就非常适合。

下面是经典的所有权被转移导致报错的例子:

fn main() {
    let s = String::from("hello, world");
    // s在这里被转移给a
    let a = Box::new(s);
    // 报错此处继续尝试将s转移给b
    let b = Box::new(s);     
}

使用Rc就可以轻易解决:

use std::rc::Rc;
fn main() {
    let a = Rc::new(String::from("hello, world"));
    let b = Rc::clone(&a);

    assert_eq!(2, Rc::strong_count(&a));
    assert_eq!(Rc::strong_count(&a),Rc::strong_count(&b))
}

以上代码我们使用Rc::new创建了一个新的Rc<String>智能指针并赋给变量a,该指针指向底层的字符串数据。

智能指针Rc<T>在创建时还会将引用计数加1此时获取引用计数的关联函数Rc::strong_count返回的值将是1

Rc::clone

接着,我们又使用Rc::clone克隆了一份智能指针Rc<String>,并将该智能指针的引用计数增加到2

由于ab是同一个智能指针的两个副本,因此通过它们两个获取引用计数的结果都是2

不要给clone字样所迷惑,以为所有的clone都是深拷贝。这里的clone仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据,因此ab是共享了底层的字符串s,这种复制效率是非常高的。当然你也可以使用a.clone()的方式来克隆,但是从可读性角度,Rc::clone的方式我们更加推荐。

实际上Rust中还有不少clone都是浅拷贝,例如迭代器的克隆.

观察引用计数的变化

使用关联函数Rc::strong_count可以获取当前引用计数的值,我们来观察下引用计数如何随着变量声明、释放而变化:

use std::rc::Rc;
fn main() {
        let a = Rc::new(String::from("test ref counting"));
        println!("count after creating a = {}", Rc::strong_count(&a));
        let b =  Rc::clone(&a);
        println!("count after creating b = {}", Rc::strong_count(&a));
        {
            let c =  Rc::clone(&a);
            println!("count after creating c = {}", Rc::strong_count(&c));
        }
        println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

有几点值得注意:

  • 由于变量c在语句块内部声明当离开语句块时它会因为超出作用域而被释放最终引用计数会减少1, 事实上这个得益于Rc<T>实现了Drop特征
  • a,b,c三个智能指针引用计数都是同样的,并且共享底层的数据,因此打印计数时用哪个都行
  • 无法看到的是: 当ab超出作用域后引用计数会变成0最终智能指针和它指向的底层字符串都会被清理释放

不可变引用

事实上,Rc<T>是指向底层数据的不可变的引用因此你无法通过它来修改数据这也符合Rust的借用规则要么多个不可变借用要么一个可变借用。

但是可以修改数据也是非常有用的,只不过我们需要配合其它数据类型来一起使用,例如内部可变性的RefCell<T>类型以及互斥锁Mutex<T>。事实上,在多线程编程中,ArcMutext锁的组合使用非常常见,既可以让我们在不同的线程中共享数据,又允许在各个线程中对其进行修改。

一个综合例子

考虑一个场景,有很多小器具,里面每个器具都有自己的主人,但是存在多个器具属于同一个主人的情况,此时使用Rc<T>就非常适合:

use std::rc::Rc;

struct Owner {
    name: String,
    // ...other fields
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>,
    // ...other fields
}

fn main() {
    // 创建一个基于引用计数的`Owner`.
    let gadget_owner: Rc<Owner> = Rc::new(
        Owner {
            name: "Gadget Man".to_string(),
        }
    );

    // 创建两个不同的工具,它们属于同一个主人
    let gadget1 = Gadget {
        id: 1,
        owner: Rc::clone(&gadget_owner),
    };
    let gadget2 = Gadget {
        id: 2,
        owner: Rc::clone(&gadget_owner),
    };

    // 释放掉第一个`Rc<Owner>`
    drop(gadget_owner);

    // 尽管在之前我们释放了gadget_owner但是依然可以在这里使用owner的信息
    // 原因是上面仅仅drop掉其中一个智能指针引用而不是drop掉owner数据外面还有两个引用指向底层的owner数据引用计数尚未清零
    // 因此owner数据依然可以被使用
    println!("Gadget {} owned by {}", gadget1.id, gadget1.owner.name);
    println!("Gadget {} owned by {}", gadget2.id, gadget2.owner.name);

    // 在函数最后,`gadget1`和`gadget2`也被释放,最终引用计数归零,随后底层数据也被清理释放
}

以上代码很好的展示了Rc<T>的用途,当然你也可以用借用的方式,但是实现起来就会复杂的多,而且随着工具在各个代码中的四处使用,引用生命周期也将变得更加复杂,毕竟结构体中的引用类型,总是令人不那么愉快,对不?

Rc简单总结

  • Rc/Arc是不可变引用,你无法修改它指向的值,只能进行读取, 如果要修改,需要配合后面章节的内部可变性RefCell或互斥锁Mutex
  • 一旦最后一个拥有者消失,则资源会自动被回收,这个生命周期是在编译期就确定下来的
  • Rc只能用于同一线程内部想要用于线程之间的对象共享, 你需要使用Arc
  • Rc<T>是一个智能指针,实现了Deref特征,因此你无需先解开Rc指针,再使用里面的T,而是可以直接使用T, 例如上例中的gadget1.owner.name

多线程无力的Rc

来看看在多线程场景使用Rc<T>会如何:

use std::rc::Rc;
use std::thread;

fn main() {
    let s = Rc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Rc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}",s)
        });
    }
}

由于我们还没有学习多线程的章节,上面的例子就特地简化了相关的实现。首先通过thread::spawn创建一个线程,然后使用move关键字把克隆出的s的所有权转移到线程中。

能够实现这一点,完全得益于Rc带来的多所有权机制,但是以上代码会报错:

error[E0277]: `Rc<String>` cannot be sent between threads safely

表面原因是Rc<T>不能在线程间安全的传递,实际上是因为它没有实现Send特征,而该特征是恰恰是多线程间传递数据的关键,我们会在多线程章节中进行讲解。

当然,还有更深层的原因: 由于Rc<T>需要管理引用计数,但是该计数器并没有使用任何并发原语,因此无法实现原子化的计数操作, 最终会导致计数错误。

好在天无绝人之路一起来看看Rust为我们提供的功能一致但是多线程安全的Arc

Arc

ArcAtomic Rc的缩写,顾名思义:原子化的Rc<T>智能指针。原子化是一种并发原语,我们在后续章节会进行深入讲解,这里你只要知道它能保证我们的数据能够安全的被线程间共享即可。

Arc的性能损耗

你可能好奇,为何不直接使用Arc,还要画蛇添足弄一个Rc还有Rust的基本数据类型、标准库数据类型为什么不自动实现原子化操作

原因在于原子化或者其它锁带来的线程安全都会伴随着性能损耗而且这种性能损耗还不小因此Rust把这种选择权交给你毕竟需要线程安全的代码其实占比并不高大部分时间我们都在跟线程内的代码执行打交道。

ArcRc拥有完全一样的API修改起来很简单:

use std::sync::Arc;
use std::thread;

fn main() {
    let s = Arc::new(String::from("多线程漫游者"));
    for _ in 0..10 {
        let s = Arc::clone(&s);
        let handle = thread::spawn(move || {
           println!("{}",s)
        });
    }
}

对了,两者还有一点区别: ArcRc并没有定义在同一个模块,前者通过use std::sync::Arc来引入,后者use std::rc::Rc.

总结

在Rust中所有权机制保证了一个数据只会有一个所有者如果你想要在图数据结构、或者多线程等中使用这种机制会成为极大的阻碍。 好在Rust为我们提供了智能指针RcArc,使用它们就能实现多个所有者共享一个数据的功能。

RcArc的区别在于,后者是原子化实现的引用计数,因此是线程安全的,可以用于多线程中共享数据。

这两者都是只读的,如果想要实现内部数据可修改,必须配合内部可变性RefCell或者互斥锁Mutex来一起使用。