https://www.jdon.com/concurrent/rust.html
Rust語言項目初始是為了解決兩個棘手問題:
1. 如何進行安全的系統編程?
2.如何實現無痛苦的並發編程
最初,這些問題似乎是正交的不相關,但是讓我們驚訝的是,最終解決方案被證明是相同的:同樣使Rust安全的工具也幫助你正面解決並發。
內存的安全錯誤和並發錯誤往往歸結為代碼訪問數據引起的問題,這是不應該的。Rust秘密武器是ownership,系統程序員需要服從的訪問控制紀律,Rust編譯器也會為你靜態地檢查。
對於內存安全,意味着你在一個沒有垃圾回收機制下編程,不用害怕segfault,因為Rust會抓住這些錯誤。
對於並發,這意味着你可以選擇各種各樣的並發范式(消息傳遞、共享狀態、無鎖、純函數式),而Rust會幫助你避免常見的陷阱。
下面是Rust的並發風格:
-
channel只傳送屬於其的消息,你能從一個線程發送指針到另外一個線程,而不用擔心這兩個線程因為同時訪問這個指針產生競爭爭奪,Rust的channel通道是線程隔離的。
-
lock知道其保護哦數據,當一個鎖被一個線程hold住,Rust確保數據只能被這個線程訪問,狀態從來不會意外地被分享,"鎖住數據,而不是代碼" 是Rust特點
-
每個數據類型都能知曉其是否可以在多線程之間安全傳輸或訪問,Rust增強這種安全用途;也就沒有數據訪問爭奪,即使對於無鎖的數據結構,線程安全不只是文檔上寫寫,而是其實在的法律規則。
-
你能在線程之間分享stack frames , Rust會確保這個frame在其他線程還在使用它時一直活躍,在Rust中即使最大膽的共享也會確保安全。
所有這些好處都是得益於Rust的所有權模型,和事實上鎖、通道channel和無鎖數據結構等之類的庫包,這意味着Rust的並發目標是開放的,新的庫包對新的范式編程更有力,也能捕獲更多bug,這些都只要使用Rust的所有權特性來增加拓展API。
背景:所有權ownership
在Rust中,每個值都有一個所有作用域(owning scope),傳送或返回一個值意味着傳送ownership所有權到一個新的作用域。當作用域結束自動銷毀時,值還是被擁有的。
讓我們看看簡單案例,假設我們創建一個vector,放入一些元素:
fn make_vec() {
let mut vec = Vec::new(); // owned by make_vec's scope
vec.push(0);
vec.push(1);
// scope ends, `vec` is destroyed
}
這個作用域創建一個值並開始擁有它,make_vec的整個部分是vec的擁有作用域,擁有者能使用vec做任何事情,包括改變它,作用域結束后,也就是到該方法結束處,vec還是被擁有,直到其自動被釋放。
如果這個vector返回或被傳送時更有趣:
fn make_vec() -> Vec<i32> {
let mut vec = Vec::new();
vec.push(0);
vec.push(1);
vec // 傳送所有權給調用者
}
fn print_vec(vec: Vec<i32>) {
// `vec`參數是這個作用域一部分, 這樣它被`print_vec`擁有
for i in vec.iter() {
println!("{}", i)
}
// 現在, `vec` 被釋放
}
fn use_vec() {
let vec = make_vec(); // 獲得vector擁有權
print_vec(vec); // 將擁有權傳給 `print_vec`
}
現在,在make_vec作用域結束之前,vec通過被返回移出了原來的作用域,它並沒有被銷毀,一個調用者如use_vec然后會接受vector的擁有權。
另外一方面,print_vec函數將vec作為輸入參數,vector擁有權被傳送到它的調用者,因為print_vec並不再傳送擁有權了,在其作用域結束后vector會被銷毀。
一旦擁有權被交出了,其值就再也不能被使用,比如考慮下面use_vec變量:
fn use_vec() {
let vec = make_vec(); // 獲得vector擁有權
print_vec(vec); // 傳送擁有權給 `print_vec`
for i in vec.iter() { // 繼續使用`vec`
println!("{}", i * 2)
}
}
編譯這段代碼會得到錯誤提示:
error: use of moved value: `vec`
for i in vec.iter() {
^~~
編譯器認為vec不能再被使用,因為其所有權已經被傳遞給到其他地方。災難得以避免。
背景:borrowing出借
到目前整個故事還不是很完美,因為將vector完全交給print_vec並不是我們真正意圖,我們只是授權print_vec臨時訪問這個vector,然后我們還想繼續使用vector,這種情況也是常常發生。
那么borrowing這個概念就來了,如果你得訪問一個值,你能將其出租給你調用的函數,Rust會檢查這些租約會不會超過被借用的對象。為了借用一個值,你得引用它,也就是某個指針,使用 &操作符:
fn print_vec(vec: &Vec<i32>) {
// `vec` 參數被借用到這個作用域
for i in vec.iter() {
println!("{}", i)
}
// 借用結束
}
fn use_vec() {
let vec = make_vec(); // 獲得vector擁有權
print_vec(&vec); // 出租訪問權給`print_vec`
for i in vec.iter() { // 繼續使用 `vec`
println!("{}", i * 2)
}
// vec在這里被釋放
}
這里實現了vector臨時出租情況。
每個引用對於有限的作用域是有效的,編譯器會自動決定,引用一般有下面兩個風格:
-
不可變引用 &T, 允許分享但是不能改變,能有多個引用 &T 同時指向同一個值,但是這些引用如果在活躍情況下(通過線程)是不能改變這個值。
-
可變引用 &mut T, 允許改變,但不能被分享,如果有一個可變引用&mut T 指向值,就不能同時有其他活躍的引用指向這個值,這個值可以被改變。
Rust會在編譯時檢查這些規則,borrowing並沒有運行額外負擔。
消息傳遞
前面我們已經具備了Rust的背景知識,現在可以看看這些概念對於Rust的並發模型意味着什么?
並發編程有很多風格,但是特別簡單的一個是消息傳遞,線程或actor會通過彼此發送消息通訊,這種風格是強調一起共享和通訊。不是通過共享內存進行通訊,而是通過通訊消息傳遞實現內存的共享。
Rust的擁有權使得其在編譯時就能夠方便發現問題,考慮下面的channel API:
fn send<T: Send>(chan: &Channel<T>, t: T);
fn recv<T: Send>(chan: &Channel<T>) -> T;
Channel是基於數據類型的泛型,它們會轉化,Send部分代碼意味着T被認為安全地在線程之間發送。
在Rust中,傳送一個T給send函數意味着傳送擁有權給它,這具有深遠的影響:它意味着像下面的代碼會產生一個編譯錯誤:
/ Suppose chan: Channel<Vec<i32>>
let mut vec = Vec::new();
// do some computation
send(&chan, vec);
print_vec(&vec);
在這里,線程創建一個vector,將其發送到另外一個線程,然后繼續使用它,這個線程接受到vector會改變它然后繼續運行,這樣調用print_vec會導致兩個線程競爭情況,這是一個use-after-free bug。
這里,Rust編譯器會產生一個錯誤警告:
Error: use of moved value `vec`
鎖
另外一個處理並發的方式是通過共享鎖進行線程通訊。共享狀態的並發有壞名聲,它一般會讓人忘記去加鎖,或者在錯誤時間實現了自己都不知道的錯誤數據,帶來災難后果。Rust是這樣實現:
-
共享狀態並發仍然是一個基本編程風格,特別對於為了最大性能的系統編碼。
-
真正問題是不小心共享了狀態。
Rust瞄准了提供給你征服共享狀態並發的直接攻擊,無論你是否使用鎖或無鎖技術。
在Rust中,線程是彼此自動隔離的,這是因為所有權。那么寫只會在這個線程能夠訪問時才會發生,訪問方式是或者擁有被訪問的數據擁有權,或者有一個可變的數據出借權,無論哪個方式,線程只會在某個時刻只有一個訪問數據。
記住:可變的出借borrow不會與其他出借同時發生,鎖也提供同樣的授權,也就是可變的排他,這是通過在運行時的同步實現的,鎖的API其實內部有一個鈎子直接通往Rust的擁有權系統。比如下面代碼:
// 創建一個新的 mutex
fn mutex<T: Send>(t: T) -> Mutex<T>;
// 獲得鎖
fn lock<T: Send>(mutex: &Mutex<T>) -> MutexGuard<T>;
// 通過鎖保護數據訪問
fn access<T: Send>(guard: &mut MutexGuard<T>) -> &mut T;
鎖的API有幾個不尋常的特點:
首先,Mutex類似是基於T類型的泛型,而T是被鎖保護的數據的類型,當你創建一個Mutex,你能傳遞這個數據的擁有權給mutex,然后會立即放棄再次訪問它的能力(鎖首次被創建時是出於解鎖狀態)。
然后,你能用lock這段代碼堵塞線程知道鎖被獲得,這個函數也不尋常提供一個返回值MutexGuard<T>,這個MutexGuard在其被銷毀時會自動釋放鎖,不會有單獨的unlock函數來做這件事。
訪問鎖只有一個途徑是通過access
函數,它會將一個guard的可變性borrow轉為數據的可變borrow:
fn use_lock(mutex: &Mutex<Vec<i32>>) {
// 獲得鎖,取得guard的擁有權;
// 鎖將被當前作用域一直持有
let mut guard = lock(mutex);
// 通過可變的出借guard來訪問數據
let vec = access(&mut guard);
// vec 有類型 `&mut Vec<i32>`
vec.push(3);
// 當`guard`被銷毀時,這里鎖自動釋放
}
這里有兩個關鍵組成:
-
通過
access
返回的可變引用生存周期不能超過借用的MutexGuard
-
鎖只會在
MutexGuard
被摧毀時釋放。
這個結果是Rust有力的鎖紀律:當有鎖把持數據時,它不會讓你有額外其他能力訪問被鎖保護的數據。所有其他的試圖訪問都會產生編譯錯誤:
fn use_lock(mutex: &Mutex<Vec<i32>>) {
let vec = {
// 獲得鎖
let mut guard = lock(mutex);
// 試圖返回數據的出借borrow
access(&mut guard)
// guard被銷毀,鎖釋放
};
// 在鎖外部試圖訪問數據
vec.push(3);
}
Rust會產生一個編譯錯誤:
error: `guard` does not live long enough
access(&mut guard)
^~~~~
線程安全和“Send”
線程安全的數據結構使用內部同步機制能夠被多線程同時並發安全地訪問,Rust提供兩種聰明的指針為引用計數:
-
Rc<T> 提供為正常讀/寫提供引用計數,不是線程安全
-
Arc<T> 為原子操作提供引用計數,它是線程安全的。
通過使用Arc的硬件原子操作會被通過Rc的香草性輕量操作有代價成本,這樣,使用Rc而不是Arc是有好處的,另外一個方面,Rc<T>從來不會從一個線程遷移到另外一個也是很關鍵,因為會導致競爭中斷計數。
大多數語言在線程安全與線程不安全之間沒有語法區分,靠的是詳細文檔。
在Rust中,世界被划分為兩種數據類型,Send意味着它們能被安全地從一個線程移到另外一個線程,而!Send意味着這么做是不安全的,如果一個類型的組件都是Send,那么那就是線程安全的,當然基本類型不會繼承線程安全,正常情況下:Arc是Send,而Rc不是。
我們已經看到Channel和Mutex只和帶有Send數據工作,因為它們跨線程訪問數據的邊界,它們也是為Send強有力的支持特性。