【譯】Arc 在 Rust 中是如何工作的


原文標題: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類型的所有引用都結束之前,它不會被丟棄——因此即使在main函數結束之后,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中。

foobar離開作用域時,Arc::drop()就被調用了,原子計數減一。如果Arc::drop()發現原子計數等於0,那么它所指向的堆上的數據(vec![0]和原子計數)會被清理並從堆上擦除。

原子計數是一種能夠讓你以線程安全的方式修改和增加它的值的類型;在對原子類型允許進行其他操作之前,前面的原子類型操作必須要全部完成;因此被稱為原子的(atomic)(即不可分割的)(操作)。

需要注意的是,Arc只能包含不可變數據。這是因為如果兩個線程試圖在同一時間修改被包含的值,Arc無法保證避免數據競爭。如果你希望修改數據,你應該在Arc類型內部封裝一個互斥鎖保護(Mutex guard)。

為什么這些東西能讓Arc是線程安全的呢?

Arc是線程安全的是因為它給編譯器保證數據的引用至少活得和數據本身一樣長(譯注:這里原作者應該是想表達,數據的引用存在期間,數據都是有效的)。這是因為每次你創建一個對堆上數據得引用,原子計數就會加一,數據只有在當原子計數等於零得時候才會被丟棄(每當一個引用離開作用域時,原子計數會減一)——Arc和一個普通得Rc(引用計數)之間得區別就在於原子計數。

那么Rc有什么用,為什么不用Arc來做所有事情?

原因是,原子計數是一個(開銷)昂貴的變量類型,而普通的usize類型則沒有這些開銷。原子類型不僅在實際的程序中占用更多的內存,而且每個原子類型的操作還需要更長的時間,因為它必須分配資源來為對其自身進行讀寫的調用維護一個隊列進而保證原子性。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM