原文標題:How Arc works in Rust
原文連結:https://medium.com/@DylanKerler1/how-arc-works-in-rust-b06192acd0a6
公眾號: Rust 碎碎念
翻譯 by: Praying
原子引用計數(Arc)型別是一種智慧指標,它能夠讓你以執行緒安全的方式線上程間共享不可變資料。我還沒有發現能夠很好地解釋它的工作原理的文章,所以我決定嘗試來寫一篇。(文章)第一部分是介紹怎樣使用Arc
和為什麼要使用Arc
;如果你已經瞭解這部分內容,只是想知道它是如何工作的,可以直接跳到第二部分:“它是怎樣工作的(How does it work)”。
為什麼你需要使用Arc?
當你試圖線上程間共享資料時,需要Arc型別來保證被共享的型別的生命週期,與執行時間最長的執行緒活得一樣久。考慮下面的例子:
use std::thread;
use std::time::Duration;
fn main() {
let foo = vec![0]; // creation of foo here
thread::spawn(|| {
thread::sleep(Duration::from_millis(20));
println!("{:?}", &foo);
});
} // foo gets dropped here
// wait 20 milliseconds
// try to print foo
這段程式碼無法編譯通過。我們會得到一個錯誤,稱foo
的引用活得比foo
自身更久。這是因為foo
在main函式結尾處就被丟棄(drop)了,並且這個被丟棄的值會在20毫秒後在生成的執行緒中被試圖訪問。這就是Arc
的作用所在。原子引用計數確保在對foo
型別的所有引用都結束之前,它不會被丟棄——因此即使在mai
n函式結束之後,foo
仍然會存在。現在考慮下面的示例:
use std::thread;
use std::sync::Arc;
use std::time::Duration;
fn main() {
let foo = Arc::new(vec![0]);
let bar = Arc::clone(&foo);
thread::spawn(move || {
thread::sleep(Duration::from_millis(20));
println!("{:?}", *bar);
});
println!("{:?}", foo);
}
在這個例子中,我們可以在(主)執行緒中引用foo
並且還可以在(子)執行緒被生成之後訪問它的值。
它是怎樣工作的?
你已經知道如何使用Arc
了,現在讓我們討論一下它是如何工作的。當你呼叫let foo = Arc::new(vec![0])
時,你同時建立了一個vec![0]
和一個值為1的原子引用計數,並且把它們都儲存在堆上的相同位置(緊挨著)。指向堆上的這份資料的指標存放在foo
中。因此,foo
是由指向一個物件的指標構成,被指向的物件包含vec![0]
和原子計數。
當你呼叫let bar = Arc::clone(&foo)
時,你是在獲取foo
的一個引用、對foo
(指向存放在堆上的資料的指標)解引用、接著找到foo
指向的地址、找出裡面存放的值(vec![0]
和原子計數)、把原子計數加一、最後把指向vec![0]
的指標儲存在bar
中。
當foo
或bar
離開作用域時,Arc::drop()
就被呼叫了,原子計數減一。如果Arc::drop()
發現原子計數等於0,那麼它所指向的堆上的資料(vec![0]
和原子計數)會被清理並從堆上擦除。
原子計數是一種能夠讓你以執行緒安全的方式修改和增加它的值的型別;在對原子型別允許進行其他操作之前,前面的原子型別操作必須要全部完成;因此被稱為原子的(atomic)(即不可分割的)(操作)。
需要注意的是,Arc
只能包含不可變資料。這是因為如果兩個執行緒試圖在同一時間修改被包含的值,Arc
無法保證避免資料競爭。如果你希望修改資料,你應該在Arc
型別內部封裝一個互斥鎖保護(Mutex guard)。
為什麼這些東西能讓Arc是執行緒安全的呢?
Arc
是執行緒安全的是因為它給編譯器保證資料的引用至少活得和資料本身一樣長(譯註:這裡原作者應該是想表達,資料的引用存在期間,資料都是有效的)。這是因為每次你建立一個對堆上資料得引用,原子計數就會加一,資料只有在當原子計數等於零得時候才會被丟棄(每當一個引用離開作用域時,原子計數會減一)——Arc
和一個普通得Rc
(引用計數)之間得區別就在於原子計數。
那麼Rc有什麼用,為什麼不用Arc來做所有事情?
原因是,原子計數是一個(開銷)昂貴的變數型別,而普通的usize型別則沒有這些開銷。原子型別不僅在實際的程式中佔用更多的記憶體,而且每個原子型別的操作還需要更長的時間,因為它必須分配資源來為對其自身進行讀寫的呼叫維護一個佇列進而保證原子性。