choosing-your-guarantees.md
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4
Rust 的一個(gè)重要特性是允許我們控制一個(gè)程序的開(kāi)銷和(安全)保證。
Rust 標(biāo)準(zhǔn)庫(kù)中有多種“wrapper 類型”的抽象,他們代表了大量在開(kāi)銷,工程學(xué)和安全保證之間的權(quán)衡。很多讓你在運(yùn)行時(shí)和編譯時(shí)增強(qiáng)之間選擇。這一部分將會(huì)詳細(xì)解釋一些特定的抽象。
在開(kāi)始之前,強(qiáng)烈建議你閱讀Rust的[所有權(quán)](Ownership 所有權(quán).md)和[借用](References and Borrowing 引用和借用.md)。
Box<T>
Box\<T>是一個(gè)“自我擁有的”,或者“裝箱”的指針。因?yàn)樗梢跃S持引用和包含的數(shù)據(jù),它是數(shù)據(jù)的唯一的擁有者。特別的,當(dāng)執(zhí)行類似如下代碼時(shí):
let x = Box::new(1);
let y = x;
// x no longer accessible here
這里,裝箱被移動(dòng)進(jìn)了y
。因?yàn)?code>x不再擁有它,此后編譯器不再允許程序猿使用x
。相似的一個(gè)函數(shù)可以通過(guò)返回裝箱來(lái)移出函數(shù)。
當(dāng)一個(gè)裝箱(還沒(méi)有被移動(dòng)的)離開(kāi)了作用域,析構(gòu)函數(shù)將會(huì)運(yùn)行。這個(gè)析構(gòu)函數(shù)負(fù)責(zé)釋放內(nèi)部的數(shù)據(jù)。
這是一個(gè)動(dòng)態(tài)分配的零開(kāi)銷抽象。如果你想要在堆上分配一些內(nèi)存并安全的傳遞這些內(nèi)存的指針,這是理想的情況。注意你將只能通過(guò)正常的借用規(guī)則來(lái)共享引用,這些在編譯時(shí)被檢查。
&T
和&mut T
這分別是不可變和可變引用。他們遵循“讀寫鎖”的模式,也就是你只可能擁有一個(gè)數(shù)據(jù)的可變引用,或者任意數(shù)量的不可變引用,但不是兩者都有。這個(gè)保證在編譯時(shí)執(zhí)行,并且沒(méi)有明顯的運(yùn)行時(shí)開(kāi)銷。在大部分情況這兩個(gè)指針類型有能力在代碼塊之間廉價(jià)的共享引用。
這些指針不能在超出他們的生命周期的情況下被拷貝。
*const T
和*mut T
這些是C風(fēng)格的指針,并沒(méi)附加生命周期或所有權(quán)。他們只是指向一些內(nèi)存位置,沒(méi)有其他的限制。他們能提供的唯一的保證是除非在標(biāo)記為unsafe
的代碼中他們不會(huì)被解引用。
他們?cè)跇?gòu)建像Vec<T>
這樣的安全,低開(kāi)銷抽象時(shí)是有用的,不過(guò)應(yīng)該避免在安全代碼中使用。
Rc<T>
這是第一個(gè)我們將會(huì)介紹到的有運(yùn)行時(shí)開(kāi)銷的包裝類型。
Rc\<T>是一個(gè)引用計(jì)數(shù)指針。換句話說(shuō),這讓我們擁有相同數(shù)據(jù)的多個(gè)“有所有權(quán)”的指針,并且數(shù)據(jù)在所有指針離開(kāi)作用域后將被釋放(析構(gòu)函數(shù)將會(huì)執(zhí)行)。
在內(nèi)部,它包含一個(gè)共享的“引用計(jì)數(shù)”(也叫做“refcount”),每次Rc
被拷貝時(shí)遞增,而每次Rc
離開(kāi)作用域時(shí)遞減。Rc<T>
的主要職責(zé)是確保共享的數(shù)據(jù)的析構(gòu)函數(shù)被調(diào)用。
這里內(nèi)部的數(shù)據(jù)是不可變的,并且如果創(chuàng)建了一個(gè)循環(huán)引用,數(shù)據(jù)將會(huì)泄露。如果我們想要數(shù)據(jù)在存在循環(huán)引用時(shí)不被泄漏,我們需要一個(gè)垃圾回收器。
這里(Rc<T>
)提供的主要保證是,直到所有引用離開(kāi)作用域后,相關(guān)數(shù)據(jù)才會(huì)被銷毀。
當(dāng)我們想要?jiǎng)討B(tài)分配并在程序的不同部分共享一些(只讀)數(shù)據(jù),且不確定哪部分程序會(huì)最后使用這個(gè)指針時(shí),我們應(yīng)該用Rc<T>
。當(dāng)&T
不可能靜態(tài)地檢查正確性,或者程序員不想浪費(fèi)時(shí)間編寫反人類的代碼時(shí),它可以作為&T
的可行的替代。
這個(gè)指針并不是線程安全的,并且Rust也不會(huì)允許它被傳遞或共享給別的線程。這允許你在不必要的情況下的原子性開(kāi)銷。
Rc<T>
有個(gè)姐妹版智能指針類型——Weak<T>
。它是一個(gè)既沒(méi)有所有權(quán)、也不能被借用的智能指針。它也比較像&T
,但并沒(méi)有生命周期的限制--一個(gè)Weak<T>
可以一直存活。然而,嘗試對(duì)其內(nèi)部數(shù)據(jù)進(jìn)行訪問(wèn)可能失敗并返回None
,因?yàn)樗梢员扔兴袡?quán)的Rc
存活更久。這對(duì)循環(huán)數(shù)據(jù)結(jié)構(gòu)和一些其他類型是有用的。
隨著內(nèi)存使用增加,Rc<T>
是一次性的分配,雖然相比一個(gè)常規(guī)Box<T>
它會(huì)多分配額外兩個(gè)字(也就是說(shuō),兩個(gè)usize
值)。(“強(qiáng)”引用計(jì)數(shù)相比“弱”引用計(jì)數(shù))。
Rc<T>
分別在拷貝和離開(kāi)作用域時(shí)會(huì)產(chǎn)生遞增/遞減引用計(jì)數(shù)的計(jì)算型開(kāi)銷。注意拷貝將不會(huì)進(jìn)行一次深度復(fù)制,相反它會(huì)簡(jiǎn)單的遞增內(nèi)部引用計(jì)數(shù)并返回一個(gè)Rc<T>
的拷貝。
Cell
提供內(nèi)部可變性。換句話說(shuō),他們包含的數(shù)據(jù)可以被修改,即便是這個(gè)類型并不能以可變形式獲取(例如,當(dāng)他們位于一個(gè)&
指針或Rc<T>
之后時(shí))。
對(duì)此cell
模塊的文檔有一個(gè)非常好的解釋。
這些類型經(jīng)常在結(jié)構(gòu)體字段中出現(xiàn),不過(guò)他們也可能在其他一些地方找到。
Cell<T>
Cell\<T>是一個(gè)提供了零開(kāi)銷內(nèi)部可變性的類型,不過(guò)只用于Copy
類型。因?yàn)榫幾g器知道它包含的值對(duì)應(yīng)的所有數(shù)據(jù)都位于棧上,所以并沒(méi)有通過(guò)簡(jiǎn)單的替換數(shù)據(jù)而導(dǎo)致任何位于引用之后的數(shù)據(jù)泄露(或者更糟!)的擔(dān)心。
然而使用這個(gè)封裝仍有可能違反你自己的不可變性,所以謹(jǐn)慎的使用它。它是一個(gè)很好的標(biāo)識(shí),表明一些數(shù)據(jù)塊是可變的并且可能在你第一次讀取它和當(dāng)你想要使用它時(shí)的值并不一樣。
use std::cell::Cell;
let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());
注意這里我們可以通過(guò)多個(gè)不可變的引用改變相同的值。
這與如下代碼有相同的運(yùn)行時(shí)開(kāi)銷:
let mut x = 1;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);
不過(guò)它有額外的優(yōu)勢(shì),它確實(shí)能夠編譯成功。(高級(jí)黑?)
這個(gè)類型放寬了當(dāng)沒(méi)有必要時(shí)“沒(méi)有因可變性導(dǎo)致的混淆”的限制。然而,這也放寬了這個(gè)限制提供的保證;所以當(dāng)你的不可變量依賴存儲(chǔ)在Cell
中的數(shù)據(jù),你應(yīng)該多加小心。
這對(duì)改變基本類型和其他Copy
類型非常有用,當(dāng)通過(guò)&
和&mut
的靜態(tài)規(guī)則并沒(méi)有其他簡(jiǎn)單合適的方法改變他們的值時(shí)。
Cell
并不讓你獲取數(shù)據(jù)的內(nèi)部引用,它讓我們可以自由改變值。
使用Cell<T>
并沒(méi)有運(yùn)行時(shí)開(kāi)銷,不過(guò)你使用它來(lái)封裝一個(gè)很大的(Copy
)結(jié)構(gòu)體,可能更適合封裝單獨(dú)的字段為Cell<T>
因?yàn)槊看螌懭攵紩?huì)是一個(gè)結(jié)構(gòu)體的完整拷貝。
RefCell<T>
RefCell\<T>也提供了內(nèi)部可變性,不過(guò)并不限制為Copy
類型。
相對(duì)的,它有運(yùn)行時(shí)開(kāi)銷。RefCell<T>
在運(yùn)行時(shí)使用了讀寫鎖模式,不像&T
/&mut T
那樣在編譯時(shí)執(zhí)行。這通過(guò)borrow()
和borrow_mut()
函數(shù)來(lái)實(shí)現(xiàn),它修改一個(gè)內(nèi)部引用計(jì)數(shù)并分別返回可以不可變的和可變的解引用的智能指針。當(dāng)智能指針離開(kāi)作用域引用計(jì)數(shù)將被恢復(fù)。通過(guò)這個(gè)系統(tǒng),我們可以動(dòng)態(tài)的確保當(dāng)有一個(gè)有效的可變借用時(shí)絕不會(huì)有任何其他有效的借用。如果程序猿嘗試創(chuàng)建一個(gè)這樣的借用,線程將會(huì)恐慌。
use std::cell::RefCell;
let x = RefCell::new(vec![1,2,3,4]);
{
println!("{:?}", *x.borrow())
}
{
let mut my_ref = x.borrow_mut();
my_ref.push(1);
}
與Cell
相似,它主要用于難以或不可能滿足借用檢查的情況。大體上我們知道這樣的改變不會(huì)發(fā)生在一個(gè)嵌套的形式中,不過(guò)檢查一下是有好處的。
對(duì)于大型的,復(fù)雜的程序,把一些東西放入RefCell
來(lái)將事情變簡(jiǎn)單是有用的。例如,Rust編譯器內(nèi)部的ctxt
結(jié)構(gòu)體中的很多map都在這個(gè)封裝中。他們只會(huì)在創(chuàng)建時(shí)被修改一次(但并不是正好在初始化后),或者在明顯分開(kāi)的地方多次多次修改。然而,因?yàn)檫@個(gè)結(jié)構(gòu)體被廣泛的用于各個(gè)地方,有效的組織可變和不可變的指針將會(huì)是困難的(也許是不可能的),并且可能產(chǎn)生大量的難以擴(kuò)展的&
指針。換句話說(shuō),RefCell
提供了一個(gè)廉價(jià)(并不是零開(kāi)銷)的方式來(lái)訪問(wèn)它。之后,如果有人增加一些代碼來(lái)嘗試修改一個(gè)已經(jīng)被借用的cell時(shí),這將會(huì)產(chǎn)生(通常是決定性的)一個(gè)恐慌,并會(huì)被追溯到那個(gè)可惡的借用上。
相似的,在Servo的DOM中有很多可變量,大部分對(duì)于一個(gè)DOM類型都是本地的,不過(guò)有一些交錯(cuò)在DOM中并修改了很多內(nèi)容。使用RefCell
和Cell
來(lái)保護(hù)所有的變化可以讓我們免于擔(dān)心到處都是的可變性,并且同時(shí)也表明了何處正在發(fā)生變化。
注意如果是一個(gè)能用&
指針的非常簡(jiǎn)單的情形應(yīng)該避免使用RefCell
。
RefCell
放寬了避免混淆的改變的靜態(tài)限制,并代之以一個(gè)動(dòng)態(tài)限制。保證本身并沒(méi)有改變。
RefCell
并不分配空間,不過(guò)它連同數(shù)據(jù)還包含一個(gè)額外的“借用狀態(tài)”指示器(一個(gè)字的大?。?。
在運(yùn)行時(shí)每次借用產(chǎn)生一次引用計(jì)數(shù)的修改/檢查。
上面的很多類型不能以一種線程安全的方式使用。特別是Rc<T>
和RefCell<T>
,他們都使用非原子的引用計(jì)數(shù)(原子引用計(jì)數(shù)可以在不引起數(shù)據(jù)競(jìng)爭(zhēng)的情況下在多個(gè)線程中遞增),不能在多線程中使用。這讓他們使用起來(lái)更廉價(jià),不過(guò)我們也需要這兩個(gè)類型的線程安全版本。他們以Arc<T>
和Mutex<T>
/RWLock<T>
的形式存在。
注意非線程安全的類型不能在線程間傳遞,并且這是在編譯時(shí)檢查的。
Arc<T>
Arc\<T>就是一個(gè)使用原子引用計(jì)數(shù)版本的Rc<T>
(Atomic reference count,因此是“Arc”)。它可以在線程間自由的傳遞。
C++的shared_ptr
與Arc
類似,然而C++的情況中它的內(nèi)部數(shù)據(jù)總是可以改變的。為了語(yǔ)義上與C++的形式相似,我們應(yīng)該使用Arc<Mutex<T>>
,Arc<RwLock<T>>
,或者Arc<UnsafeCell<T>>
1。最后一個(gè)應(yīng)該只被用在我們能確定使用它并不會(huì)造成內(nèi)存不安全性的情況下。記住寫入一個(gè)結(jié)構(gòu)體不是一個(gè)原子操作,并且很多像vec.push()
這樣的函數(shù)可以在內(nèi)部重新分配內(nèi)存并產(chǎn)生不安全的行為,所以即便是單一環(huán)境也不足以證明UnsafeCell
是安全的。
類似Rc
,它提供了當(dāng)最后的Arc
離開(kāi)作用域時(shí)(不包含任何的循環(huán)引用)其內(nèi)部數(shù)據(jù)的析構(gòu)函數(shù)將被執(zhí)行的(線程安全的)保證。
使用原子引用計(jì)數(shù)有額外的開(kāi)銷(無(wú)論是被拷貝或者離開(kāi)作用域時(shí)都會(huì)發(fā)生)。當(dāng)在一個(gè)單獨(dú)的線程中通過(guò)一個(gè)Arc
共享數(shù)據(jù)時(shí),任何時(shí)候都更傾向于使用&
指針。
Mutex<T>
和RwLock<T>
Mutex\<T>和RwLock\<T>通過(guò)RAII guard(guard是一類直到析構(gòu)函數(shù)被調(diào)用時(shí)能保持一些狀態(tài)的對(duì)象)提供了互斥功能。對(duì)于這兩個(gè)類型,mutex直到我們調(diào)用lock()
之前它都是無(wú)效的,此時(shí)直到我們獲取鎖這個(gè)線程都會(huì)被阻塞,同時(shí)它會(huì)返回一個(gè)guard。這個(gè)guard可以被用來(lái)訪問(wèn)它的內(nèi)部數(shù)據(jù)(可變的),而當(dāng)guard離開(kāi)作用域鎖將被釋放。
{
let guard = mutex.lock();
// guard dereferences mutably to the inner type
*guard += 1;
} // lock released when destructor runs
RwLock
對(duì)多線程讀有額外的效率優(yōu)勢(shì)。只要沒(méi)有writer,對(duì)于共享的數(shù)據(jù)總是可以安全的擁有多個(gè)reader;同時(shí)RwLock
讓reader們獲取一個(gè)“讀取鎖”。這樣的鎖可以并發(fā)的獲取并通過(guò)引用計(jì)數(shù)記錄。writer必須獲取一個(gè)“寫入鎖”,它只有在所有reader都離開(kāi)作用域時(shí)才能獲取。
這兩個(gè)類型都提供了線程間安全的共享可變性,然而他們易于產(chǎn)生死鎖。一些額外的協(xié)議層次的安全性可以通過(guò)類型系統(tǒng)獲取。
他們?cè)趦?nèi)部使用類原子類型來(lái)維持鎖,這樣的開(kāi)銷非常大(他們可以阻塞處理器所有的內(nèi)存讀取知道他們執(zhí)行完畢)。而當(dāng)有很多并發(fā)訪問(wèn)時(shí)等待這些鎖也將是很慢的。
閱讀Rust代碼時(shí)的一個(gè)常見(jiàn)的痛苦之處是遇到形如Rc<RefCell<Vec<T>>>
這樣的類型(或者諸如此類的更復(fù)雜的組合)。這些組合式干什么的,和為什么作者會(huì)選這么一個(gè)類型(以及何時(shí)你應(yīng)該在自己的代碼中使用這樣一個(gè)類型)的理由并不總是顯而易見(jiàn)的。
通常,將你需要的保證組合到一起是一個(gè)例子,而不為無(wú)關(guān)緊要的東西產(chǎn)生開(kāi)銷。
例如,Rc<RefCell<T>>
就是一個(gè)這樣的組合。Rc<T>
自身并不能可變的解引用;因?yàn)?code>Rc<T>可以共享,而共享的可變性可以導(dǎo)致不安全的行為,所以我們?cè)谄渲蟹湃?code>RefCell<T>來(lái)獲得可以動(dòng)態(tài)驗(yàn)證的共享可變性。現(xiàn)在我們有了共享的可變數(shù)據(jù),不過(guò)它只能以只有一個(gè)writer(沒(méi)有reader)或多個(gè)reader的方式共享。
現(xiàn)在,我們可以更進(jìn)一步,并擁有Rc<RefCell<Vec<T>>>
或Rc<Vec<RefCell<T>>>
,他們都是可共享可改變的vector,不過(guò)他們并不一樣。
前者,RefCell<T>
封裝了Vec<T>
,所以Vec<T>
整體是可變的。與此同時(shí),同一時(shí)刻只能有一個(gè)整個(gè)Vec
的可變借用。這意味著你的代碼不能同時(shí)通過(guò)不同的Rc
句柄來(lái)操作vector的不同元素。然而,我們可以隨意的從Vec<T>
中加入或取出元素。這類似于一個(gè)有運(yùn)行時(shí)借用檢查的&mut Vec<T>
。
后者,借用作用于單獨(dú)的元素,不過(guò)vector整體是不可變的。因此,我們可以獨(dú)立的借用不同的元素,不過(guò)我們對(duì)vector加入或取出元素。這類似于&mut [T]
2,不過(guò)同樣會(huì)在運(yùn)行時(shí)做借用檢查。
在并發(fā)程序中,我們有一個(gè)使用Arc<Mutex<T>>
的類似場(chǎng)景,它提供了共享可變性和所有權(quán)。
當(dāng)閱讀使用這些類型的代碼時(shí),一步步的閱讀并關(guān)注他們提供的保證/開(kāi)銷。
當(dāng)選擇一個(gè)組合類型的時(shí)候,我們必須反過(guò)來(lái)思考;搞清楚我們需要何種保證,以及在組合中的何處我們需要他們。例如,如果面對(duì)一個(gè)Vec<RefCell<T>>
和RefCell<Vec<T>>
之間的選擇,我們需要明確像上面講到的那樣的權(quán)衡并選擇其一。
Arc<UnsafeCell<T>>
實(shí)際上并不能編譯因?yàn)?code>UnsafeCell<T>并不是Send
或Sync
的,不過(guò)我們可以把它 wrap 進(jìn)一個(gè)類型并且手動(dòng)為其實(shí)現(xiàn)Send
/Sync
來(lái)獲得Arc<Wrapper<T>>
,它的Wrapper
是struct Wrapper<T>(UnsafeCell<T>)
。 ↩
&[T]
和&mut [T]
是切片(slice);他們包含一個(gè)指針和一個(gè)長(zhǎng)度并可以引用一個(gè)vector或數(shù)組的一部分。&mut [T]
能夠改變它的元素,不過(guò)長(zhǎng)度不能改變。 ↩