In this fourth Crust of Rust video, we cover smart pointers and interior mutability, by re-implementing the Cell, RefCell, and Rc types from the standard library. As part of that, we cover when those types are useful, how they work, and what the equivalent thread-safe versions of these types are. In the process, we go over some of the finer details of Rust's ownership model, and the UnsafeCell type. We also dive briefly into the Drop Check rabbit hole (https://doc.rust-lang.org/nightly/#the-rustonomicon) before coming back up for air.
你在 Rust 世界中經常遇到的一些類型,也許你對它們有一些了解,但它們是如此普遍,你需要了解它們,像是 Arc, RC, RefCell, Mutex, Cell, DeRef, AsRef, Borrow, Cow, Sized …。想要好好地了解它們的話,最好的方法就是自己去實作其中幾個,也就是本章節要做的事情。
Rustacean Station : Discord 連結
本次要實作的內容 : Rc, RefCell, Cell
本次要討論的內容 : Arc, Mutex
本次要觀看的內容 : AsRef, Deref, borrow, Cow
我們會先從 cell 實作開始,因為它有最少的怪癖,但要實作之前,先看一下 interior mutability 是什麼,我們才會知道自己要做什麼。
:question:你擁有的主要三個的是 Cell, RefCell 以及 Mutex 。Mutex 在 Sync 下,而不是 Cell,因為它就是用來解決同步問題的,Mutex 主要由 OS 或 CPU 提供,來確保操作符合預期。不過 Mutex 有點屬於 Cell,你可以將 Mutex 視為 Cell 類型,一種 interior mutability 的型別。
Q : If we could outline why using one versus the other would be great. Shared mutation sounds fine, but there seem to be a number of specialised tools to a similar problem
A : 我們等等來了解不同 Cell 類型、不同 interior mutability 的類型,它們的限制是什麼。Cell, RefCell, Mutex 對於可以放入其中的東西有不同的限制,以及使用方法也有所不同。一般來說,如果你離 Mutex 越遠,你就可以越自由的放任何東西進去,不過要讓型別可以正常工作,所需的後勤工作的開銷也會相對增加,這就有點像是要將用到 lock 的 linked list,想讓 linked list 變成 lock-free 的話,實作會變得非常的麻煩。
Q : what about box?
A : box 不提供 interior mutability,如果你的 box 裡面有共享參考,你就不能去更改 box 裡面的東西。
Q : is there a way to tell that a supposedly immutable struct has some stuff inside it that is mutable like a Cell?
A : 沒有 ! 您無法從類型的外部知道它是否具有 interior mutability。
看一下 Cell 提供的一些函式的簽章 :
當你看了所有 Cell 的方法之後,你會發現,沒有任何方法是會回傳 Cell 裡面的值的參考,你可以取代裡面的值、拿到裡面複製的值,但你不可以有指標指向 Cell 裡面的值,這很重要,因為如果你不能指向 Cell 裡面的值,這樣操作起來就會很安全,確保了 Cell 在操作值的過程中,沒有指標指向它,在這基礎之上,改變 Cell 裡面的值是安全的。以上這些都是在編譯時期就可以被保證的。
這就像是如果你有一個 queue,這個 queue 若想要確保 thread safety,你不能讓想要增減 queue 的人直接操作 buffer,只能讓他們透過 enqueue()
以及 dequeue()
的方法讓他們達成目的,但 enqueue()
和 dequeue()
就是要透過一些 Mutex 機制來保證在多個執行緒執行完畢後,其結果能符合預期。
接著看到這個實作 :
Cell 是不支援多執行緒去執行的,所以就不會有資料爭用的問題。
Q : Question: Why can't we borrow as mut more then once for Rc if it's only one thread
A : 這裡沒有 RC 也沒有借用。一般情況下,你不會使用對 Cell 的 exclusive 參考。如果你擁有對 Cell 的 exclusive 參考,你就可以拿到 Cell 裡面的值得 exclusive 參考,但是這有一個壞處,你將無法呼叫 set()
, get()
… 方法
Q : In the example you mentioned, you say that if there is a single thread, then there is no need to worry about multiple references to a cell. In that case, what benefit does using Cell…
A : 好處就是你可以有多個東西的共享參考,這個好處也帶來衍伸的優點,第一個優點是你可以把 Cell 存到很多地方,第二個優點是在不同地方或不同資料結構的多個指標都可以指向它。不過因為單執行緒的時候,你知道在一個時間點內,只有一個參考會被使用,Cell 讓你可以在 safe 的程式碼裡面去改變那個值。這看起來有點迂迴,但其實要回到 Rust 要保證只有一個 exclusive 的參考,Cell 就是要讓你繞過編譯器的檢查,因為 Cell 只被限制能在被單一執行緒被執行,而 Rust 之所以要保證只有一個 exclusive 的參考就是要確保 thread safety,但你已經被限制只有單一執行緒,那也就不用考慮 thread safety,因為單一執行緒一定是符合預期的。
Q : So Cell should usually be used for small copy types?
A : 是的,因為你在呼叫 get()
函式的時候,會牽涉到記憶體複製的問題。
Q : is there a Sync version of Cell?
A : 沒有。
開始建置 Rust 專案 :
src/lib.rs :
src/cell.rs :
幾乎所有這些提供 interior mutability 的類型的核心是一種稱為 UnsafeCell 的特殊 Cell 型別。UnsafeCell 持有一些型別,你可以隨時獲得指向這些型別的原始 exclusive 指標。當你知道這樣做是安全的時候,你可以將其轉換為 exclusive 的 Rust 參考。
引入 UnsafeCell 到我們剛剛的實作,這樣我們才可以去改變 immutable 參考的值 :
Q : Is there a "classic" example of when someone would want to use a Cell?
A : 通常用在很小的值,例如數字、旗標,這些值可能會在很多地方被改動。Cell 很常被用在 Thread local,在 thread local,你知道只有一個執行緒會去存取那個值,你可能會想保存 thread local 的狀態,比如說旗標、計數器或其他東西。但 thread local 只會給你那個值的共享參考,因為一個執行緒可能會想要多次拿取 thread local,Cell 就是個很好的方法去提供 mutability。
Q : So why Cell has a as_ptr
method?
A : 它會回傳原始指標指向 Cell 裡面的值,如果想要呼叫這個方法,你就必須要在 unsafe block 裡面做。為什麼暴露原始指標是可以的 ? 因為只有 unsafe block 可以操作原始指標。
回到實作 :
我們想要讓以下這個 test case 得到錯誤 :
src/lib.rs :
src/cell.rs :
我們明明還沒限制 Cell 只能在單一執行緒執行,測試程式竟然有得到以下錯誤 :
為什麼我們明明還沒實作 !Sync
,我們的 Cell 就已經支援了只能單一執行緒執行的功能呢 ? 先探討一下編譯器怎麼看待 impl<T> !Sync for Cell<T> {}
,編譯器只有 nightly 支援 impl<T> !Sync for Cell<T> {}
這個語法 (編譯器會提醒你 negative trait bounds are not yet fully implemented; use marker types for now),想要擺脫這個情形的作法是你在 impl block 裡面放一個 thread unsafe 的值。至於什麼型別是 thread unsafe,就是 UnsafeCell
!
因為我們已經在我們的 struct 裡面放 UnsafeCell
的值了,所以我們早就已經有 !Sync
了。就不用在另外實作 impl<T> !Sync for Cell<T> {...}
了。
新增一個單一執行緒也會錯的 test case :
Q : can you unsafe impl Sync to show your test failing?
A : 看以下實驗
驗證 set_during_get
結果 :
驗證 concurrent_set_take2
,因為資料爭用 (x
的參考),導致交錯的結果 (因為記憶體系統過快導致沒展示出我們想展示的交錯) :
驗證 concurrent_get_set
,因為資料爭用導致非預期的結果 :
註解掉 unsafe impl<T> Sync for Cell<T> {}
,讓 Cell 不能多執行緒執行 :
Q : What is the point of allowing T to be non-Copy (like String) if we only have the .get() method for Copy types?
A : 為什麼 struct 不宣告成這樣 : pub struct Cell<T: Copy>
? 我們是可以這麼做,只有泛型是 Copy 型態的才能呼叫 Cell,但只有 get
方法會用到 Copy 的 bound,我們希望我們的 bound 只放在需要的地方就好。
Q : an you explain why we need UnsafeCell and cannot just cast (unsafely) the &T to a mutable reference/pointer?
A : 在 Rust,唯一的方法可以正確地將共享參考轉換成 exclusive 參考是一定要使用 UnsafeCell,因為 Rust 本來就永不允許你將共享參考直接轉換成 exclusive 參考,這條件保證了編譯器在最佳化的時候不會出錯。
Struct std::cell::RefCell : A mutable memory location with dynamically checked borrow rules。
Q : Cell the type which had special compiler instructions or was it something else?
A : 沒有。UnsafeCell 是特別的型別,但 Cell 不是。
正常來說,所有的 borrow checking 是在編譯時期做的。但 RefCell 讓你等到執行時期才去做 borrow checking,如果你正在走訪一張循環圖,這將會很方便 : 可能在遞迴的早期,您已經獲得了對此節點的 mutable 參考,但稍後你嘗試對同一節點 (因為循環圖) 採用 mutable 參考,並且 RefCell 將捕獲這種情況,但假設你現在有個圖,你知道它沒有循環,這時候你就不會取道同一節點的 mutable 參考。不過編譯器並不知道你在走訪的過程中會不會因為循環而拿到 mutable 參考,這時的 RefCell 就可以滿足這個需求。
Q : Actually RefCell sounds like a good use-case for single-threaded binary trees with cycles
A : 沒錯。
src/lib.rs :
src/refcell.rs :
Q : Why can't we use the borrow and borrowmut trait here?
A : 這兩個 trait 是為了一些非常不同的東西,等等你就會知道為什麼。
Q : are you intentionally deviating from the RefCell api in std lib?
A : 沒有,是想透過簡化程式碼來讓你了解 RefCell 的原理。
開始實作 borrow 以及 borrow_mut :
想法是這樣,但實作有兩大問題 :
這兩個問題都出在 self.state
修改的那幾行。
想要一次解決兩個問題,只要使用我們剛剛實作的 Cell 即可 :
Q : could you use an AtomicIsize to make it thread-safe?
A : 等講到 Mutex 的部分會來談。Mutex 基本上是 rw lock 以及 thread safe 版本的 RefCell
Q : when using something like Rayon, would using RefCell/Cell make no sense? Would you just rely on Mutex?
A : 對於 Rayon,您需要 thread safe 的東西,例如 mutex, rw lock 或者其它的同步化原始物件;而 RefCell 不是 thread safe,也不需要是。
剛剛的程式碼其實還有一個問題,就是我們的實作只會增加參考計數器的值,並沒有減少參考計數器的值,所以一旦你呼叫了 borrow()
,你之後就再也不能呼叫 borrow_mut()
了。
所以我們的 borrow()
不能只是回傳共享參考,borrow_mut()
不能只回傳 mutable 參考,因為呼叫者若拿到這些參考後,我們將無法追蹤他們的使用情形,所以我們需要回傳其他型別,也就是 Ref 型別和 RefMut 型別。
先新增定義 Ref 型別和 RefMut 型別以及相關實作 :
再修改 borrow 以及 borrow_mut 函式 :
但現在呼叫者拿到的是 Ref 和 RefMut 型態,那呼叫者怎麼拿到 T 的值 ? 因為呼叫者本來就是想要拿到 T 值,而不是這兩種我們定義的型態,而要解決這個問題的方法是實作 Deref/DererMut trait :
Q : Is it common practice to write SAFETY comments for every unsafe use?
A : 是的,非常建議你這樣做。如果你的程式有 unsafe 區塊,您應該確保記錄保持 safe 的要求。
src/lib.rs :
src/refcell.rs :
Rc
stands for ‘Reference Counted’
Rc<T>
provides shared ownership of a value of type T, allocated in the heap. Invoking clone on Rc produces a new pointer to the same allocation in the heap. When the last Rc
pointer to a given allocation is destroyed, the value stored in that allocation (often referred to as “inner value”) is also dropped.Rc
is no exception: you cannot generally obtain a mutable reference to something inside an Rc
. If you need mutability, put a Cell
or RefCell
inside the Rc
; see an example of mutability inside an Rc.RC
適合用在一個元素出現在多個地方的資料結構,假設在你的程式有一個大的 blob,你應該不會想有很多它的複本,而是有很多指標指向 blob,但問題是,你要在什麼時間點去釋放那個儲存在 heap 空間的 blob ? 答案是,當全部指向 blob 的指標全部消失,那你要怎麼知道已經沒有指標指向 blob ? RC
就是提供這功能。
但關鍵的是,RC
並不是 Sync 以及 Sent (後面回談到),基本上 RC
並不是 thread safe,RC
只能在單一執行緒做參考計數,但即便是在單一執行緒,對資料的上下文, 圖以及其它資料結構也是非常有幫助的。
Q : How does RC handle cyclic references?
A : 事實並非如此,如果你有一個循環,那麼循環只會阻止它被釋放。標準函式庫的 RC
有 Weak 智慧指標以及 Strong 智慧指標。它們的差別是,Weak 智慧指標不會阻止東西被刪除,而 Strong 智慧指標則會。如果 Strong 智慧指標的計數變成 0,那東西就會被 deallocate;而 Weak 智慧指標,你在使用它們之前,必須將它們 upgrade
為實指標,並且 upgrade
將會失敗。更詳細資訊請自行參考官方文件。
src/lib.rs :
src/rc.rs :
Q : RC's are quite useful when building GTK apps since passing a reference to a closure is somewhat tricky. Wrap your value in a RC, clone and send to your closure.
A : 在任何像單一執行緒這樣的東西中,例如通常是 GUI 迴圈,Rc 非常適合這種東西。
Q : if you could explain the difference between reference types such as &mut T
, *mut T
and *const T
and if there are any more
A :
&
則表示共享參考,你必須保證沒有 exclusive 參考指向該型態;類似地,型態前面有 &mut
則表示 exclusive 參考,你知道它沒有其它的共享參考指向該值。*mut T
和 *const T
不是參考,它們是原始指標,所以它並沒有像 &
/&mut
保證共享/exclusive 參考指向該型態。舉例來說,如果你現在有 *mut
/*const
,你沒辦法保證沒有其它 *mut
/*const
指向同一個值,但你對 *
也無能為力。所以當你有原始指標,你可以做的事是使用 unsafe block 去解參考它,將它轉成參考,這行為是 unsafe,所以你需要記錄下你做了什麼確保它是 safe 的。*mut
和 *const
的差異是 *mut
是你可能能夠改變的東西,你可能擁有 exclusive 參考的東西;*const
則表明這個值永遠不能被改變。一般來說,你不能將 constant 指標轉成 exclusive 參考,反之則不然。Q : What does Box provide for us?
A : heap 記憶體配置。
src/lib.rs :
src/rc.rs :
接著使用我們前面實作的 Cell 讓程式只能在單一執行緒執行 :
Q : Isn't unsafe a pretty weird keyword name. It just means something the compiler cannot guarantee is safe, not that it actually is unsafe.
A : unsafe 關鍵字確實有點奇怪,它真實的意思是,我已經自己檢查了 unsafe block 裡面的程式是 safe 的,它並不是表示 unsafe block 裡面的程式是 unsafe。
目前程式遇到跟前面 RefCell 只有遞增參考計數器,而沒有遞減的問題,這將導致 Box 永遠不會被釋放,為 Rc 實作 drop 函式即可解決 :
Q : what is the relationship between Box::into_raw and Box::leak
A : Box::into_raw
給你原始指標,你將可以對它做任何事,包含改變它的值。Box::leak
給你 static 共享參考指向 heap 記憶體,當你記憶體洩漏,它將活到到整個程式結束,但給 static 共享參考是可以的,因為 static 暗示著這個共享參考也是活到整個程式結束,但你不能改變它的值,因為它是完全共享的參考。
無檢查點,因為 Rc 還無法編譯。
*mut T
but non-zero and covariant.NonNull
的目的是為了最佳化,編譯器會知道,指標指向的值不可能是 Null,不像 *mut
可能指向 0 或 Null 值。所以編譯器可以使用它的 nullptr 作為其他值來使用。舉例來說 : Option<NonNull<T>>
, 編譯器可以使用 nullptr 來表示 None,這將讓 Option 沒有額外開銷。NonNull
有點像 *mut
:
加入 NonNull
結構到現有的程式碼 :
Q : Can't we leak a mutable reference to the value in RefCell by calling deref on RefMut, storing the return value somewhere, and then dropping RefMut?
A : 不行,回到 src/refcell.rs :
回傳值 mut Self::Target
參考的生命週期最長只能跟傳入值 mut self
(RefMut) 參考一樣。如果您嘗試將從 DerefMut 返回的值儲存到某處,然後卸除 RefMut,並嘗試再次使用 &'a mut Self::Target,編譯器會說這是不允許的,因為你正在嘗試在它所綁定的生命週期已經過期之後 (mut self 參考已經消失了) 使用 mut Self::Target 參考。
Q : if we have a mut pointer why do we need a Cell?
A : 在 src/rc.rs 中,我們沒有使用 mutable 指標,其原因是改變它的值並不安全。這又可以談到mutable 指標與 mutable 參考的不同,其差異在於 mutable 參考保證沒有任何其它東西並行地改變值,它就是個 exclusive 參考;但 mutable 指標並沒有這種保證,mutable 指標只是具有一定語意的指標,我們稱之為 *mut
,它不帶有額外的意義 (保證它是 exclusive ),這就是允許你改變值的原因。
Q : since memory is size aligned in rust, can the compiler fit other "NonNull" variants in (0, 1, 2, 3) for example when the NonNull points to a 4 byte u32?
A : 這個在 unsafe working group 被討論,Jon 認為他們還沒有對此做出裁決。
src/lib.rs :
src/rc.rs :
Struct std::marker::PhantomData
:bulb: 補充說明 : drop check
:warning: 非常複雜 :warning:
src/lib.rs :
src/rc.rs :
:pencil2: GitHub comment
Q : @jonhoo Thanks for your wonderful videos. I have a question about rc: I found I can still use inner and can even set and get inner.value in Rc's Drop, why? I added the example code in // Why? section: Rust Playground Code
A : Ah, yes, that's because I was being silly — drop(inner) doesn't actually do anything when inner is a reference. It doesn't remove it from scope, even though it kind of reads that way. The better way to safe-guard this code would be to intentionally shadow inner with: let inner = ();
Q : @jonhoo 👍 But what I still don't understand is why I can still set and get refcount after Box::from_raw. I thought the heap memory has been freed, so we can't access it. But I can still use it without panic.
A : Ah, that's because freeing memory just makes it available for future allocations, it doesn't actually make the memory inaccessible. The memory is still there, it's just undefined behavior to access it.
Q : Would it be sufficient to have PhantomData<T>
instead of PhantomData<InnerRc<T>>
?
A : 有一個 Pull Request 的內容是將 <T>
-> wrapper<T>
,會有這個改變是因為有人可能意外地實作了 :
如果有人實作了這個 Drop
,然後你的 struct 又是 <T>
:
這樣 Drop for RcInner<T>
就不會被檢查了。
Q : this only needed only when T is not 'static ?
A : 是的,但現在我們希望 Rc 能支援任何類型,所以才需要 PhantomData
。
標準函式庫的 Rc 的 T 是 ?Sized (動態長度) :
想要自己實作 Rc 支援 ?Sized
相當不容易,困難點需參照 Trait std::ops::CoerceUnsized
Q : The real question for me, is that should this problem exist in the first place? I'm feeling that if I write the same thing in C it will be clearer and I won't be wasting time in all of these.
A : 你鮮少會在你的程式碼寫像這次實作這麼複雜的東西,之所以那麼複雜是因為我們在實作 Rust 的 low level primitive,這個問題不會出現在你身上。重要的是你可以用 Rust 寫這些限制,但如果是 C 的話,編譯器會假設你是大人了,就不幫你做這些檢查。所以 C 才會在執行時期 crash 發生的頻率會高於 Rust,因為 Rust 都在編譯時期就幫你檢查好了。
Q : what is the difference between !Sized and ?Sized
A : !Sized
表示 not Sized,?Sized
表示它不一定要是 Sized,因為預設所有東西都有 Sized bound。
Option<Ref<'_, T>>/Option<RefMut<'_, T>>
變成 Ref<'_, T>/RefMut<'_, T>
,至於為什麼不是回傳 Option 來避免取到 None 值,是因為 rw lock 會 block 目前的執行緒 borrow/borrow_mut
沒有成功,直到某些條件變數滿足才會 unblock 該執行緒。borrow_mut
,所以你不需要記錄現在有幾個 reader (共享參考),因為現在就只有某個執行緒能擁有那個值。Mutex 也有 block 的機制,如果你當前無法獲得 lock,則你就會被 block 直到持有 lock 的執行緒釋放 lock。Q : can you guarantee thread safety acros ffi boundaries? Or is that just, whatever (e.g C code) calls into you needs to uphold some guarantees?
A : 沒有保證。跨越 FFI 邊界的一切本質上都是 unsafe。
Q : Why would you ever prefer Rc over Arc?
A : Rc 開銷較小。atomic 操作的代價很大。
Q : Is there an async RwLock?
A : 現在的 crate 已經支援 : async_rwlock
std::borrow
有點像智慧指標,來看看標準函式庫的 std::borrow:Cow
:
看一下 Cow 的一些實作 :
如果 COW 本身持有對其他內容的參考,你可以獲得對 COW 的共享參考,它只是通過該參考提供存取權;但如果 COW 擁有其包含的內容,那麼它將向您提供對該內容的 exclusive 參考。
Cow 的使用時機是,如果你的程式大部分時間都是 read,偶爾才需要 write。這常常發生在字串操作,假設你有 escape
函式 :
回傳值改為 Cow 型態 :
Q : so why does from_utf8_lossy return Cow but other from_utf etc variants don't?
A : 直接看以下程式碼 :
回顧 :
:pencil2: GitHub comment
Q : How come for RefCell and Cell Drop is not implemented but it is for Rc?
A : Because neither RefCell nor Cell need to do anything special on Drop — they just have to drop their inner type, which happens automatically when dropping their inner UnsafeCell.