聊聊 Rust 共享所有權之 Rc 和 Arc

1v1 所有權

Rust中所有權約束了值只能有一個所有者,當值離開作用域時,它將被銷燬。

像如下代碼,字符串a如果直接移動給b後就沒法後邊再去打印,因爲它的所有權已經轉移給了b

let a = String::from("hello");
let b = a;
println!("a = {}, b = {}", a, b);

// got error:
// error[E0382]: borrow of moved value: `a`
// 3 |     let a = String::from("hello");
//   |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
// 4 |     let b = a;
//   |             - value moved here
// 5 |     println!("a = {}, b = {}", a, b);
//   |                                ^ value borrowed here after move

深拷貝(clone)

如果clone的話可以複製一份,但是這樣的話就需要開闢一塊新的內存,不是很高效。

let a = String::from("hello");
// let b = a;
let b = a.clone();
println!("a = {}, b = {}", a, b);
// output: a = hello, b = hello

引用計數 (reference count)

想要實現多個所有者,又開銷小,可以用引用計數,對應的類型是Rc

Rc只會在複製時增加引用計數,當引用計數爲 0 時,會自動調用drop方法,釋放內存。

let a = Rc::new(String::from("hello"));
let _b = Rc::clone(&a);
{
    let _b = Rc::clone(&a);
    println!("reference count {}", Rc::strong_count(&a));
    // 3, will be 2 after this block out of scope
}
println!("reference count {}", Rc::strong_count(&a)); // 2

寫時複製(copy on write)

Rc引用的值是不可變的,如果想要修改,可以使用Rc::make_mut方法,它會檢查引用計數,在有別的有效引用(strong)時,會複製一份,然後修改。否則就直接修改原來的值。這也是寫時複製,只有在需要修改時纔會複製。

let mut a = Rc::new(String::from("hello"));
let b = Rc::clone(&a);
//  allocate a new string (copy on write)
(*Rc::make_mut(&mut a)).push_str( " world");
println!("{} {}",  a, b);
// hello world hello

let c = Rc::clone(&a);
println!("{} {} {}",  a, b, c);
// hello world hello hello world

所以這麼用有一個好處,如果有修改,修改是獨立於之前的引用的,不用擔心修改會影響之前引用的值。

當然,如果想保持值修改的同步,可以使用之前提到的CellRefCell,這兩個類型可以實現內部可變性,可以在不可變引用的情況下修改值。

循環引用

Rc是不允許循環引用的,因爲它的引用計數是在編譯時就確定的,如果有循環引用,那麼引用計數永遠不會爲 0,也就永遠不會調用drop方法,導致內存泄漏。

這裏用官方的一個例子說明:下邊代碼用來描述工具(gadget)和工具所有者(owner)的關係,一個工具可以有一個個所有者,一個所有者可以有多個工具。

如果用Rc來實現的話,會出現循環引用,工具和工具所有者互相引用,導致誰都無法對引用計數減一,也就無法釋放對應的內存。

use std::{rc::Rc, cell::RefCell};

struct Owner {
    name: String,
    gadgets: RefCell<Vec<Rc<Gadget>>>
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>
}

fn main() {
    let gadget_owner : Rc<Owner> = Rc::new(
            Owner { name: String::from("Gadget Man"), gadgets: RefCell::new(vec![]) }
    );
    // 兩個工具,都有同一個所有者
    let gadget1 = Rc::new(Gadget { id: 1, owner: gadget_owner.clone() });
    let gadget2 =Rc::new(Gadget { id: 2, owner: gadget_owner.clone() });

    gadget_owner.gadgets.borrow_mut().push(gadget1.clone());
    gadget_owner.gadgets.borrow_mut().push(gadget2.clone());
    // 釋放gadget_owner的引用計數,保留工具的owner引用計數
    drop(gadget_owner);

    println!("strong count of gadget1: {}", Rc::strong_count(&gadget1));
    // strong count of gadget1: 2

    println!("strong count of gadget1.owner: {}", Rc::strong_count(&gadget1.owner));
    // strong count of gadget1.owner: 2

    // 釋放gadget1的引用計數,正常沒有引用循環的話,owner對應的引用計數也需要釋放
    // 但是gadget1的owner的引用計數不會減一,導致內存泄漏
    drop(gadget1);

    println!("strong count of gadget2.owner: {}", Rc::strong_count(&gadget2.owner));
    // strong count of gadget2.owner: 2
}

循環引用如下圖所示

gadgets和owner的引用形成了一個環,誰也沒法釋放,對應的引用計數無法減到0,也就沒法釋放

+-----------+       +-----------+
|   Owner   |<------|  Gadget   |
|           |       |           |
|   Rc      |       |   Rc      |
|           |       |           |
| gadgets --|------>| owner ----+
+-----------+       +-----------+

弱引用

這個時候就是弱引用的用武之地了,弱引用不會增加引用計數,所以不會導致循環引用。

但是它也不能保證引用的值一定存在,因爲它的引用計數可能爲 0,所以用時,需要用upgrade方法來獲取Option類型的引用。

也就是說引用的值釋放與否只取決於強引用的引用計數。

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

struct Owner {
    name: String,
    gadgets: RefCell<Vec<Weak<Gadget>>>
}

struct Gadget {
    id: i32,
    owner: Rc<Owner>
}

fn main() {
    let gadget_owner : Rc<Owner> = Rc::new(
            Owner {
                name: "Gadget Man".to_string(),
                gadgets: RefCell::new(Vec::new())
            }
    );

    let gadget1 = Rc::new(Gadget{id: 1, owner: gadget_owner.clone()});
    let gadget2 = Rc::new(Gadget{id: 2, owner: gadget_owner.clone()});

    gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget1.clone()));
    gadget_owner.gadgets.borrow_mut().push(Rc::downgrade(&gadget2.clone()));

    for gadget_opt in gadget_owner.gadgets.borrow().iter() {
        let gadget = gadget_opt.upgrade().unwrap();
        println!("Gadget {} owned by {}", gadget.id, gadget.owner.name);
    }
    drop(gadget_owner);
    println!("strong count of gadget1: {}", Rc::strong_count(&gadget1));
    // strong count of gadget1: 1
    println!("strong count of gadget1.owner: {}", Rc::strong_count(&gadget1.owner));
    // strong count of gadget1.owner: 2
    drop(gadget1);

    println!("strong count of gadget2.owner: {}", Rc::strong_count(&gadget2.owner));
    // strong count of gadget2.owner: 1
}

線程安全

Rc是線程不安全的,如果想要在多線程中使用,可以使用Arc,它是Rc的線程安全版本。(A代表atomic

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

fn main() {
    let val = Arc::new(5);

    for _ in 0..3 {
        let val = Arc::clone(&val);
        thread::spawn(move || {
            let v = *val.as_ref() + 1;
            println!("{v:?}");
        });
    }
    thread::sleep(std::time::Duration::from_secs(1));
}

而如果想要在多線程中修改值,可以使用MutexRwLock,它們都是線程安全的。如Arc<Mutex<T>>


最後還有一點想提下,Rc<T>Arc<T>都實現了自動解引用DerefT,所以可以直接在Rc<T>Arc<T>上調用T的方法。而爲了防止方法名衝突,一般習慣用全限定語法調用方法來調用Rc<T>Arc<T>的方法,如Rc::clone

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/zr7_9xbWYgYMAOVBda4Dxg