Rc 對比 Arc(reference counting)
/ Rc -> "R-C", Arc -> "ark" /
所有權說每個值恰好有一個擁有者,那很乾淨——但有些資料沒有單一的自然擁有者。一張圖裡的節點被其他好幾個節點指著;一個設定物件被程式許多部分使用;你想讓這份資料活到「其中任何一個」仍需要它為止,並只在最後一個用完時才釋放。共享所有權的工具是參考計數:一個帶有「目前有幾個擁有者」計數的值,在計數降到零時自動釋放。Rust 給你兩個這種東西——Rc<T> 與 Arc<T>——而唯一真正的差別在於它們是否跨執行緒安全。
兩者在概念上以同樣方式運作。Rc::new(value) 把值放上堆積、計數為 1;Rc::clone(&r) 「不」複製值——它造出指向同一個值的另一個把手、把計數加到 2;當每個把手離開範圍時,它的 drop 把計數遞減;當計數到 0 時,值被釋放。許多把手、一個值、在最後一次 drop 時釋放。差別在計數器的實作。Rc(參考計數)用普通、非原子的遞增來更新一個一般整數——快,但只在單一執行緒上正確,因為兩條執行緒對那個整數競爭會損毀它。所以 Rc 既非 Send 也非 Sync,編譯器禁止把它移過執行緒邊界。Arc(原子參考計數)用原子操作來更新計數,在並行存取下安全,但每次 clone/drop 多花一點。所以 Arc 「是」Send 與 Sync(當它的內容是時),是你用於跨執行緒共享所有權的那一個。名字就編碼了這點:Arc 的「A」是「atomic」。
為何重要:Rc/Arc 正是你在真的需要共享所有權時逃離單一擁有者規則的方式,而在兩者之間選擇就只是「單執行緒 -> Rc(較便宜),多執行緒 -> Arc(執行緒安全)」;編譯器會藉由駁回跨執行緒傳送的 Rc 替你執行這個選擇。兩個誠實的提醒。第一,兩者給你的都是共享的「讀取」存取;要「改動」共享資料,你必須把它們和內部可變性結合——單執行緒上用 Rc<RefCell<T>>、跨執行緒用 Arc<Mutex<T>>。第二,參考計數無法釋放一個參考「環」:若兩個 Rc 值互相指著對方,它們的計數永遠到不了零,記憶體就洩漏。解法是 Weak(一個不保活該值的非擁有參考),用來打破環。參考計數是被弄安全的共享所有權,而不是垃圾回收器。
use std::rc::Rc; use std::sync::Arc; use std::thread; let a = Rc::new(vec![1, 2, 3]); let b = Rc::clone(&a); // 計數 2;同一個 Vec,「沒」被複製 println!("count = {}", Rc::strong_count(&a)); // 2 let shared = Arc::new(vec![1, 2, 3]); let s = Arc::clone(&shared); thread::spawn(move || println!("{:?}", s)).join().unwrap(); // Arc 跨執行緒 // thread::spawn(move || println!("{:?}", b)); // Rc「無法」編譯
同一個共享所有權想法;Rc 用快速的非原子計數(單執行緒),Arc 用原子計數(多執行緒)。clone 增加的是計數,不是資料。
Rc/Arc 給的是共享「讀取」;要改動共享資料得加上內部可變性(Rc<RefCell<T>> 或 Arc<Mutex<T>>)。而參考計數會洩漏參考環——互指的兩個把手永遠到不了零;用 Weak 打破它。