剖析智能指針 Rc Weak 與 Arc
我們知道 rust ownership 有三原則:
-
每個值 value, 都有一個所有者 owner
-
同一時間,一個值只能有一個所有者 owner
-
當所有者 owner 離開作用域,對應的值會自動 dropped
但有些時候一個值,要被多個變量共享。同時無法用引用來解決,因爲不確定哪個變量最後結束,無法確定生命週期。所以這時候 Rc
reference count 智能指針派上用場了, Rc
共享所有權,每增加一個引用時,計數加一,離開作用域時減一,當引用計數爲 0 時執行析構函數
Rc
線程不安全,跨線程時需要使用 Arc
, 其中 A
是 atomic 原子的意思
查看引用計數
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("https://mytechshares.com/"));
println!("ref count is {}", Rc::strong_count(&a));
let b = Rc::clone(&a);
println!("ref count is {}", Rc::strong_count(&a));
{
let c = Rc::clone(&a);
println!("ref count is {}", Rc::strong_count(&a));
println!("ref count is {}", Rc::strong_count(&c));
}
println!("ref count is {}", Rc::strong_count(&a));
}
strong_count
查看引用計數,變量 c
在 inner 詞法作用域內,在離開作用域前打印查看引用計數
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `target/debug/hello_cargo`
ref count is 1
ref count is 2
ref count is 3
ref count is 3
ref count is 2
每增加一次引用,計數都會加一,在 inner 語句塊中,count 是 3,但當離開後,計數減爲 2
循環引用
熟悉 Garbage Collection 的都知道,有的 GC 算法就靠的引用計數,比如 python 的實現。比如 Redis object 同樣用 rc 來實現,由於 redis rc
並不暴露給用戶,只要寫代碼時注意引用方向,就不會寫出循環引用,但是 rust Rc
就無法避免,先來看個例子
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
}
impl Drop for Node {
fn drop(&mut self) {
println!("Dropping https://mytechshares.com 董澤潤的技術筆記");
}
}
fn main() {
let first = Rc::new(RefCell::new(Node {next: None}));
let second = Rc::new(RefCell::new(Node {next: None}));
let third = Rc::new(RefCell::new(Node {next: None}));
(*first).borrow_mut().next = Some(Rc::clone(&second));
(*second).borrow_mut().next = Some(Rc::clone(&third));
(*third).borrow_mut().next = Some(Rc::clone(&first));
}
這是一個環形鏈表的代表,稍微有點繞,原理就是 first -> second -> third, 同時 third -> first, 代碼運行後,並沒有打印 Dropping https://mytechshares.com 董澤潤的技術筆記
這裏面 RefCell
, borrow_mut
有點繞,剛學 rust 時一直讀不懂,下一篇會詳細講解。簡單說就是 RefCell
, Cell
提供了一種機制叫 內部可變性
,因爲 Rc
共享變量所有權,所以要求只能讀不允許修改。那麼 Rc
裏面的值包一層 RefCell
, Cell
就可以避開編譯器檢查,變成運行時檢查,相當於開了個後門。Rust 裏大量使用這種設計模式
那麼如何 fix 這個循環引用呢?答案是 Weak
指針,只增加引用邏輯,不共享所有權,即不增加 strong reference count
use std::rc::Rc;
use std::rc::Weak;
use std::cell::RefCell;
struct Node {
next: Option<Rc<RefCell<Node>>>,
head: Option<Weak<RefCell<Node>>>,
}
impl Drop for Node {
fn drop(&mut self) {
println!("Dropping https://mytechshares.com 董澤潤的技術筆記");
}
}
fn main() {
let first = Rc::new(RefCell::new(Node {next: None, head: None}));
let second = Rc::new(RefCell::new(Node {next: None, head: None}));
let third = Rc::new(RefCell::new(Node {next: None, head: None}));
(*first).borrow_mut().next = Some(Rc::clone(&second));
(*second).borrow_mut().next = Some(Rc::clone(&third));
(*third).borrow_mut().head = Some(Rc::downgrade(&first));
}
這是修復後的代碼,增加一個 head
字段,Weak
類型的指針,通過 Rc::downgrade
來生成 first 的弱引用
# cargo run
Compiling hello_cargo v0.1.0 (/root/zerun.dong/code/rusttest/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.63s
Running `target/debug/hello_cargo`
Dropping https://mytechshares.com 董澤潤的技術筆記
Dropping https://mytechshares.com 董澤潤的技術筆記
Dropping https://mytechshares.com 董澤潤的技術筆記
運行後看到,打印了三次 Dropping 信息,符合預期。另外由於 Weak
指針指向的對象可能析構了,所以不能直接解引用,要模式匹配,再 upgrade
線程安全 Arc
讓我們看一個併發修改變量的例子,來自 the rust book 官網
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("董澤潤的技術筆記 Got Result: {}", *counter.lock().unwrap());
}
spawn 開啓 10 個線程,併發對 counter 加一,最後運行後打印
# cargo run
Compiling hello_cargo v0.1.0 (/root/zerun.dong/code/rusttest/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 3.72s
Running `target/debug/hello_cargo`
董澤潤的技術筆記 Got Result: 10
剛學的時候確實很繞,連併發修改變量都這麼麻煩,各種 Arc
套 Mutex
, 其實這都是爲了實現零成本的運行時安全,總要有人做 GC 方面的工作
而且各種 wrapper 並不妨礙閱讀源碼,忽略就好,關注代碼邏輯,只有寫的時候才需要仔細思考
底層實現
#[stable(feature = "rust1", since = "1.0.0")]
pub struct Rc<T: ?Sized> {
ptr: NonNull<RcBox<T>>,
phantom: PhantomData<RcBox<T>>,
}
#[repr(C)]
struct RcBox<T: ?Sized> {
strong: Cell<usize>,
weak: Cell<usize>,
value: T,
}
Rc
是一個結構體,兩個字段 ptr
, phantom
成員都是 RcBox
類型,注意看裏面有 strong
, weak
計數字段和真實數據 value
字段,就裏再次看到了 Cell
來實現的內部可變量
PhantomData
是幽靈數據類型,參考 nomicon 文檔,大致場景就是:
在編寫非安全代碼時,我們常常遇見這種情況:類型或生命週期邏輯上與一個結構體關聯起來了,但是卻不屬於結構體的任何一個成員。這種情況對於生命週期尤爲常見。
PhantomData 不消耗存儲空間,它只是模擬了某種類型的數據,以方便靜態分析。這麼做比顯式地告訴類型系統你需要的變性更不容易出錯,而且還能提供 drop 檢查需要的信息
Zero-sized type used to mark things that "act like" they own a `T`.
Adding a `PhantomData<T>` field to your type tells the compiler that your
type acts as though it stores a value of type `T`, even though it doesn't
really. This information is used when computing certain safety properties.
簡單說 PhantomData
就是零長的佔位符,告訴編譯器看起來我擁有這個 T, 但不屬於我,同時如果析構時,也要調用 drop 釋放 T. 因爲幽靈字段的存在,Rc
是不擁有 RcBox
的,NonNull
告訴編譯器,這個指針 ptr 一定不爲空,你可以優化,Option<Rc<T>>
跟 Rc<T>
佔用相同的大小,去掉了是否爲空的標記字段
pub fn new(value: T) -> Rc<T> {
// There is an implicit weak pointer owned by all the strong
// pointers, which ensures that the weak destructor never frees
// the allocation while the strong destructor is running, even
// if the weak pointer is stored inside the strong one.
Self::from_inner(
Box::leak(box RcBox { strong: Cell::new(1), weak: Cell::new(1), value }).into(),
)
}
從初始化 Rc::new
代碼中看到,初始 strong, weak 計數值均爲 1
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Clone for Rc<T> {
#[inline]
fn clone(&self) -> Rc<T> {
self.inner().inc_strong();
Self::from_inner(self.ptr)
}
}
impl<T: ?Sized> Rc<T> {
#[inline(always)]
fn inner(&self) -> &RcBox<T> {
// This unsafety is ok because while this Rc is alive we're guaranteed
// that the inner pointer is valid.
unsafe { self.ptr.as_ref() }
}
}
fn inc_strong(&self) {
let strong = self.strong();
// We want to abort on overflow instead of dropping the value.
// The reference count will never be zero when this is called;
// nevertheless, we insert an abort here to hint LLVM at
// an otherwise missed optimization.
if strong == 0 || strong == usize::MAX {
abort();
}
self.strong_ref().set(strong + 1);
}
Rc::clone
並不會拷貝數據,只增加 strong ref count 強引用計數
#[stable(feature = "rust1", since = "1.0.0")]
unsafe impl<#[may_dangle] T: ?Sized> Drop for Rc<T> {
fn drop(&mut self) {
unsafe {
self.inner().dec_strong();
if self.inner().strong() == 0 {
// destroy the contained object
ptr::drop_in_place(Self::get_mut_unchecked(self));
// remove the implicit "strong weak" pointer now that we've
// destroyed the contents.
self.inner().dec_weak();
if self.inner().weak() == 0 {
Global.deallocate(self.ptr.cast(), Layout::for_value(self.ptr.as_ref()));
}
}
}
}
}
可以看到 drop
時先做計數減一操作,如果 strong ref count 到 0,開始析構釋放對象。同時 weak 減一,如果爲 0, 就要釋放 RcBox
, 也就是 RcBox
和 Value T
是單獨釋放的
pub struct Arc<T: ?Sized> {
ptr: NonNull<ArcInner<T>>,
phantom: PhantomData<ArcInner<T>>,
}
struct ArcInner<T: ?Sized> {
strong: atomic::AtomicUsize,
// the value usize::MAX acts as a sentinel for temporarily "locking" the
// ability to upgrade weak pointers or downgrade strong ones; this is used
// to avoid races in `make_mut` and `get_mut`.
weak: atomic::AtomicUsize,
data: T,
}
而 Arc
裏面的計數是用 atomic 來保證原子的,所以是併發字全。
#[stable(feature = "rc_weak", since = "1.4.0")]
pub struct Weak<T: ?Sized> {
// This is a `NonNull` to allow optimizing the size of this type in enums,
// but it is not necessarily a valid pointer.
// `Weak::new` sets this to `usize::MAX` so that it doesn’t need
// to allocate space on the heap. That's not a value a real pointer
// will ever have because RcBox has alignment at least 2.
// This is only possible when `T: Sized`; unsized `T` never dangle.
ptr: NonNull<RcBox<T>>,
}
#[stable(feature = "rc_weak", since = "1.4.0")]
pub fn downgrade(this: &Self) -> Weak<T> {
this.inner().inc_weak();
// Make sure we do not create a dangling Weak
debug_assert!(!is_dangling(this.ptr.as_ptr()));
Weak { ptr: this.ptr }
}s
Rc::downgrade
生成 Weak
邏輯也比較簡單,inc_weak
增加 weak ref count 弱引用計數,然後返回 Weak
還有很多源碼實現,理解有點難,多 google 查查,再讀讀註釋,感興趣自行查看
小結
本文先分享這些,寫文章不容易,如果對大家有所幫助和啓發,請大家幫忙點擊在看
,點贊
,分享
三連
關於 Rc/Arc 智能指針
大家有什麼看法,歡迎留言一起討論,如果理解有誤,請大家指正 ^_^
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/hjOLVK2FTZsyaNkJXJB2QQ