自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

連續(xù) 3 年最受歡迎:Rust,香!

新聞 前端
我們在選擇一種開發(fā)語言時會綜合考量各方面的特性,根據(jù)實際的需求適當取舍。魚和熊掌往往不可兼得,要想開發(fā)效率高,必然要犧牲性能和資源消耗,反之亦然。

 [[334421]]

我們在選擇一種開發(fā)語言時會綜合考量各方面的特性,根據(jù)實際的需求適當取舍。魚和熊掌往往不可兼得,要想開發(fā)效率高,必然要犧牲性能和資源消耗,反之亦然。但是Rust卻出其不意,令人眼前一亮!本文將從性能、內(nèi)存安全、開發(fā)效率、跨平臺性及生態(tài)等五個方面,對Rust這一編程語言進行一些科普性質(zhì)的分享。

一 性能對比

不同的語言使用不同的內(nèi)存管理方式,一些語言使用垃圾回收機制在運行時尋找不再被使用的內(nèi)存并釋放,典型的如Java、Golang。在另一些語言中,程序員必須親自分配和釋放內(nèi)存,比如C/C++。Rust 則選擇了第三種方式:內(nèi)存被一個所有權(quán)系統(tǒng)管理,它擁有一系列的規(guī)則使編譯器在編譯時進行檢查,任何所有權(quán)系統(tǒng)的功能都不會導(dǎo)致運行時開銷。Rust 速度驚人且內(nèi)存利用率極高,標準Rust性能與標準C++性能不相上下,某些場景下效率甚至高于C++。由于沒有運行時和垃圾回收,它能夠勝任對性能要求特別高的服務(wù)。網(wǎng)上已經(jīng)有了很多關(guān)于Rust性能分析對比的文章,不過為了獲得一手的資料,還是自己動手來的更加真實。我選擇了Python,C++,Golang這3種語言來和Rust做性能對比。

性能測試場景設(shè)計

同樣的算法用4種語言分別實現(xiàn),對比在規(guī)定的時間內(nèi)完成任務(wù)的次數(shù)。本次測試選擇的算法是找出10000000以內(nèi)的所有素數(shù),比較在一分鐘內(nèi)完成找出所有素數(shù)任務(wù)的次數(shù)。

源代碼鏈接見[1]。

靜態(tài)編譯(或者打包)后生成的二進制大小對比

結(jié)論:(二進制大小)python > golang > rust > c++

運行速度對比

本場景下比較1分鐘內(nèi)找出1000000以內(nèi)所有素數(shù)的次數(shù)。

結(jié)論:(運行效率)rust > c++ > golang > python

重點來了,在3臺不同的機器上測試四次的結(jié)果顯示:Rust效率居然高于C++?。?!

內(nèi)存消耗對比(粗略計算)

結(jié)論:(內(nèi)存消耗) python > golang > rust > c++

CPU消耗對比(粗略計算)

結(jié)論:(CPU消耗)golang > python > rust = c++

以上便是我的測試結(jié)果,測試代碼、二進制和測試結(jié)果參考附件bin.zip,第一次測試后看到結(jié)果,有些吃驚,rust的性能居然超過了c++,不可思議,于是又在網(wǎng)上搜索,找到了別人已經(jīng)完成的rust性能測試,網(wǎng)上的結(jié)果更讓人吃驚,先看第一篇,原始鏈接見[2]。

我直接截圖看結(jié)論:

以上為Rust vs Golang。

以上為Rust vs C++。

結(jié)論:以上截圖顯示,Rust在性能和資源消耗上不僅大幅度優(yōu)于Golang,并且和C++性能不相上下,某些場景下效率甚至優(yōu)于C++。

以上兩種測試場景只是測試一些簡單的算法,接下來我們看一下在實際使用中的性能資源占用對比,依然是在網(wǎng)上找到了一篇測試報告[3],該測試報告用Python、PyPy、Go、Rust四種語言實現(xiàn)了一個web后端,接下來使用wrk分別對四個http服務(wù)器進行壓測,該測試場景比較貼近實際,直接截圖看結(jié)論:

結(jié)論(性能):在實際作為后端服務(wù)使用的場景下,Rust比Golang依然有明顯性能優(yōu)勢。

結(jié)論(資源占用):在內(nèi)存占用上Rust的優(yōu)勢更加明顯,只用了Golang的1/3。

綜合以上3個測試,Rust在運行效率和資源消耗上的優(yōu)勢十分明顯,和C++同一個級別,遠遠優(yōu)于Golang !

二 內(nèi)存安全性

Rust 最重要的特點就是可以提供內(nèi)存安全保證,而且沒有額外的性能損失。在傳統(tǒng)的系統(tǒng)級編程語言( C/C++) 的開發(fā)過程中,經(jīng)常出現(xiàn)因各種內(nèi)存錯誤引起的崩潰或bug ,比如空指針、野指針、內(nèi)存泄漏、內(nèi)存越界、段錯誤、數(shù)據(jù)競爭、迭代器失效等,血淚斑斑,數(shù)不勝數(shù);內(nèi)存問題是影響程序穩(wěn)定性和安全性的重大隱患,并且是影響開發(fā)效率的重大因素;根據(jù)google和微軟 兩大巨頭的說法,旗下重要產(chǎn)品程序安全問題70%由內(nèi)存問題引發(fā)[4], 并且兩個巨頭都用利用Rust語言來解決內(nèi)存安全問題的想法。Rust語言從設(shè)計之初就把解決內(nèi)存安全作為一個重要目標,通過一系列手段保證內(nèi)存安全,讓不安全的潛在風險在編譯階段就暴露出來。接下來根據(jù)自己粗淺的理解,簡單介紹Rust解決內(nèi)存安全的手段有哪些。

1 所有權(quán)規(guī)則

1)Rust 中每一個值或者對象都有一個稱之為其 所有者(owner)的變量。

例如:

  1. let obj = String::from("hello"); 

obj是String對象的所有權(quán)變量。

2)值或?qū)ο笥星抑荒苡幸粋€所有者。

3)當所有者離開作用域,所有者所代表的對象或者值會被立即銷毀。

4)賦值語句、函數(shù)調(diào)用、函數(shù)返回等會導(dǎo)致所有權(quán)轉(zhuǎn)移,原有變量會失效。

例如:

  1. fn main() { 
  2.  
  3. let s = String::from("hello"); 
  4.  
  5. let s1 = s; //所有權(quán)發(fā)生了轉(zhuǎn)移,由s轉(zhuǎn)移給s1 
  6.  
  7. print!("{}",s); //s無效,不能訪問,此句編譯會報錯 
  8.  

 

  1. fn test(s1:String){ 
  2.  
  3. print!("{}",s1); 
  4.  
  5.  
  6. fn main() { 
  7.  
  8. let s = String::from("hello"); 
  9.  
  10. test(s); //傳參,所有權(quán)發(fā)生了轉(zhuǎn)移 
  11.  
  12. print!("{}",s); //此處s無效,編譯報錯 
  13.  

Rust的所有權(quán)規(guī)則保證了同一時刻永遠只有一個變量持有一個對象的所有權(quán),避免數(shù)據(jù)競爭。

2 借用規(guī)則

可能大家都發(fā)現(xiàn)了問題,什么鬼,為什么我傳了個參數(shù)s給test函數(shù),這參數(shù)s后面還不能用了呢?如果我接下來要使用變量s怎么辦?這時候就要用到Rust的借用特性。在Rust中,你擁有一個變量的所有權(quán),如果想讓其它變量或者函數(shù)訪問,你可以把它“借”給其它變量或者你所調(diào)用的函數(shù),供它們訪問。Rust會在編譯時檢查所有借出的值,確保它們的壽命不會超過值本身的壽命。

例如,以下的寫法就沒有問題:

  1. fn test(s1:&String){ 
  2.  
  3. print!("{}",s1); 
  4.  
  5.  
  6. fn main() { 
  7.  
  8. let s = String::from("hello"); 
  9.  
  10. test(&s); //傳參,注意只是傳遞了引用,所有權(quán)還歸屬于s 
  11.  
  12. print!("{}",s); //此處s依然有效,可以訪問 
  13.  

 

  1. fn main() { 
  2.  
  3. let s = String::from("hello"); 
  4.  
  5. let s1 = &s; //s1借用s,所有權(quán)還歸屬于s 
  6.  
  7. print!("{}",s); //此處s依然有效,可以訪問 
  8.  
  9. print!("{}",s1); //此處s1和s指向同一個對象 
  10.  

如果我們嘗試修改借用的變量呢?

  1. fn main() { 
  2.  
  3. let s = String::from("hello"); 
  4.  
  5. change(&s); 
  6.  
  7.  
  8. fn change(some_string: &String) { 
  9.  
  10. some_string.push_str(", world"); 
  11.  

借用默認是不可變的,上面的代碼編譯時會報錯:

  1. error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable 
  2.  
  3. --> error.rs:8:5 
  4.  
  5.  
  6. 7 | fn change(some_string: &String) { 
  7.  
  8. | ------- use `&mut String` here to make mutable 
  9.  
  10. 8 | some_string.push_str(", world"); 
  11.  
  12. | ^^^^^^^^^^^ cannot borrow as mutable 

根據(jù)編譯錯誤的提示,通過mut關(guān)鍵字將默認借用修改為可變借用就OK,如下代碼可以編譯通過:

  1. fn main() { 
  2.  
  3. let mut s = String::from("hello"); 
  4.  
  5. change(&mut s); 
  6.  
  7.  
  8. fn change(some_string: &mut String) { 
  9.  
  10. some_string.push_str(", world"); 
  11.  

不過可變引用有一個很大的限制:在特定作用域中的特定數(shù)據(jù)有且只能有一個可變引用,這個限制的好處是 Rust 可以在編譯時就避免數(shù)據(jù)競爭,這些代碼會失敗:

  1. let mut s = String::from("hello"); 
  2.  
  3. let r1 = &mut s; 
  4.  
  5. let r2 = &mut s; 

報錯如下:

  1. error[E0499]: cannot borrow `s` as mutable more than once at a time 
  2.  
  3. --> borrow_twice.rs:5:19 
  4.  
  5.  
  6. 4 | let r1 = &mut s; 
  7.  
  8. | - first mutable borrow occurs here 
  9.  
  10. 5 | let r2 = &mut s; 
  11.  
  12. | ^ second mutable borrow occurs here 
  13.  
  14. 6 | } 
  15.  
  16. | - first borrow ends here 

在存在指針的語言中,容易通過釋放內(nèi)存時保留指向它的指針而錯誤地生成一個 懸垂指針(dangling pointer),所謂懸垂指針是其指向的內(nèi)存可能已經(jīng)被分配給其它持有者或者已經(jīng)被釋放。相比之下,在 Rust 中編譯器確保引用永遠也不會變成懸垂狀態(tài):當我們擁有一些數(shù)據(jù)的引用,編譯器確保數(shù)據(jù)不會在其引用之前離開作用域。

讓我們嘗試創(chuàng)建一個懸垂引用,Rust 會通過一個編譯時錯誤來避免:

  1. fn main() { 
  2.  
  3. let reference_to_nothing = dangle(); 
  4.  
  5.  
  6. fn dangle() -> &String { 
  7.  
  8. let s = String::from("hello"); 
  9.  
  10. &s 
  11.  

這里是編譯錯誤:

  1. error[E0106]: missing lifetime specifier 
  2.  
  3. --> dangle.rs:5:16 
  4.  
  5.  
  6. 5 | fn dangle() -> &String { 
  7.  
  8. | ^ expected lifetime parameter 
  9.  
  10.  
  11. = help: this function's return type contains a borrowed value, but there is 
  12.  
  13. no value for it to be borrowed from 
  14.  
  15. = help: consider giving it a 'static lifetime 

讓我們簡要的概括一下之前對引用的討論,以下3條規(guī)則在編譯時就會檢查,違反任何一條,編譯報錯并給出提示。

1)在任意給定時間,只能 擁有如下中的一個:

  • 一個可變引用。
  • 任意數(shù)量的不可變引用。

2)引用必須總是有效的。

3)引用的壽命不會超過值本身的壽命。

3 變量生命周期規(guī)則

生命周期檢查的主要目標是避免懸垂引用,考慮以下示例 中的程序,它有一個外部作用域和一個內(nèi)部作用域,外部作用域聲明了一個沒有初值的變量 r,而內(nèi)部作用域聲明了一個初值為 5 的變量 x。在內(nèi)部作用域中,我們嘗試將 r 的值設(shè)置為一個 x 的引用。接著在內(nèi)部作用域結(jié)束后,嘗試打印出 r 的值:

  1. error[E0106]: missing lifetime specifier 
  2.  
  3. --> dangle.rs:5:16 
  4.  
  5.  
  6. 5 | fn dangle() -> &String { 
  7.  
  8. | ^ expected lifetime parameter 
  9.  
  10.  
  11. = help: this function's return type contains a borrowed value, but there is 
  12.  
  13. no value for it to be borrowed from 
  14.  
  15. = help: consider giving it a 'static lifetime 

當編譯這段代碼時會得到一個錯誤:

  1. error: `x` does not live long enough 
  2.  
  3.  
  4. 6 | r = &x; 
  5.  
  6. | - borrow occurs here 
  7.  
  8. 7 | } 
  9.  
  10. | ^ `x` dropped here while still borrowed 
  11.  
  12. ... 
  13.  
  14. 10 | } 
  15.  
  16. | - borrowed value needs to live until here 

編譯錯誤顯示:變量 x 并沒有 “活的足夠久”,那么Rust是如何判斷的呢?

編譯器的這一部分叫做 借用檢查器(borrow checker),它比較作用域來確保所有的借用都是有效的。如下:r 和 x 的生命周期注解,分別叫做 'a 和 'b:

  1.  
  2. let r; // -------+-- 'a 
  3.  
  4. // | 
  5.  
  6. // | 
  7.  
  8. let x = 5// -+-----+-- 'b 
  9.  
  10. r = &x; // | | 
  11.  
  12. // -+ | 
  13.  
  14. // | 
  15.  
  16. println!("r: {}", r); // | 
  17.  
  18. // -------+ 

我們將 r 的生命周期標記為 'a 并將 x 的生命周期標記為 'b。如你所見,內(nèi)部的 'b 塊要比外部的生命周期 'a 小得多。在編譯時,Rust 比較這兩個生命周期的大小,并發(fā)現(xiàn) r 擁有生命周期 'a,不過它引用了一個擁有生命周期 'b 的對象。程序被拒絕編譯,因為生命周期 'b 比生命周期 'a 要小:被引用的對象比它的引用者存在的時間更短。

關(guān)于借用生命周期檢查,Rust還有一套復(fù)雜的生命周期標記規(guī)則,使Rust能在編譯時就能發(fā)現(xiàn)可能存在的懸垂引用,具體鏈接見[5]。

4 多線程安全保證

內(nèi)存破壞很多情況下是由數(shù)據(jù)競爭(data race)所引起,它可由這三個行為造成:

  • 兩個或更多指針同時訪問同一數(shù)據(jù)。
  • 至少有一個這樣的指針被用來寫入數(shù)據(jù)。
  • 不存在同步數(shù)據(jù)訪問的機制。

那么在多線程環(huán)境下,Rust是如何避免數(shù)據(jù)競爭的?

先從一個簡單的例子說起,嘗試在另一個線程使用主線程創(chuàng)建的 vector:

  1. use std::thread; 
  2.  
  3. fn main() { 
  4.  
  5. let v = vec![123]; 
  6.  
  7. let handle = thread::spawn(|| { 
  8.  
  9. println!("Here's a vector: {:?}", v); 
  10.  
  11. }); 
  12.  
  13. handle.join().unwrap(); 
  14.  

閉包使用了 v,所以閉包會捕獲 v 并使其成為閉包環(huán)境的一部分。因為 thread::spawn 在一個新線程中運行這個閉包,所以可以在新線程中訪問 v。然而當編譯這個例子時,會得到如下錯誤:

  1. error[E0373]: closure may outlive the current function, but it borrows `v`, 
  2.  
  3. which is owned by the current function 
  4.  
  5. --> src/main.rs:6:32 
  6.  
  7.  
  8. 6 | let handle = thread::spawn(|| { 
  9.  
  10. | ^^ may outlive borrowed value `v` 
  11.  
  12. 7 | println!("Here's a vector: {:?}", v); 
  13.  
  14. | - `v` is borrowed here 
  15.  
  16.  
  17. help: to force the closure to take ownership of `v` (and any other referenced 
  18.  
  19. variables), use the `move` keyword 
  20.  
  21.  
  22. 6 | let handle = thread::spawn(move || { 
  23.  
  24. | ^^^^^^^ 

Rust 會“推斷”如何捕獲 v,因為 println! 只需要 v 的引用,閉包嘗試借用 v。然而這有一個問題:Rust 不知道這個新建線程會執(zhí)行多久,所以無法知曉 v 的引用是否一直有效。所以編譯器提示:

closure may outlive the current function, but it borrows `v` 。

下面展示了一個 v 的引用很有可能不再有效的場景:

  1. use std::thread; 
  2.  
  3. fn main() { 
  4.  
  5. let v = vec![123]; 
  6.  
  7. let handle = thread::spawn(|| { 
  8.  
  9. println!("Here's a vector: {:?}", v); 
  10.  
  11. }); 
  12.  
  13. drop(v); // 強制釋放變量v 
  14.  
  15. handle.join().unwrap(); 
  16.  

為了修復(fù)示上面的編譯錯誤,我們可以聽取編譯器的建議:

  1. help: to force the closure to take ownership of `v` (and any other referenced 
  2.  
  3. variables), use the `move` keyword 
  4.  
  5.  
  6. 6 | let handle = thread::spawn(move || { 

接下來是正確的寫法:

  1. use std::thread; 
  2.  
  3. fn main() { 
  4.  
  5. let v = vec![123]; 
  6.  
  7. let handle = thread::spawn(move || { //使用 move 關(guān)鍵字強制獲取它使用的值的所有權(quán),接下來就可以正常使用v了 
  8.  
  9. println!("Here's a vector: {:?}", v); 
  10.  
  11. }); 
  12.  
  13. handle.join().unwrap(); 
  14.  

從上面簡單例子中可以看出多線程間參數(shù)傳遞時,編譯器會嚴格檢查參數(shù)的生命周期,確保參數(shù)的有效性和可能存在的數(shù)據(jù)競爭。

大家注意到?jīng)]有,上面的例子雖然能正確編譯通過,但是有個問題,變量v的所有權(quán)已經(jīng)轉(zhuǎn)移到子線程中,main函數(shù)已經(jīng)無法訪問v,如何讓main再次擁有v呢?如果用C++或者Golang等語言,你可以有很多種選擇,比如全局變量,指針,引用之類的,但是Rust沒有給你過多的選擇,在Rust中,為了安全性考慮,全局變量為只讀不允許修改,并且引用不能直接在多線程間傳遞。Rust 中一個實現(xiàn)消息傳遞并發(fā)的主要工具是 通道(channel),這種做法時借鑒了Golang的通道,用法類似。

示例:

  1. use std::thread; 
  2.  
  3. use std::sync::mpsc; 
  4.  
  5. fn main() { 
  6.  
  7. let (tx, rx) = mpsc::channel(); 
  8.  
  9. thread::spawn(move || { 
  10.  
  11. let val = String::from("hi"); 
  12.  
  13. tx.send(val).unwrap(); 
  14.  
  15. }); 
  16.  
  17. let received = rx.recv().unwrap(); 
  18.  
  19. println!("Got: {}", received); 
  20.  

上例中,我們可以在main函數(shù)中通過channel得到了子線程中的對象val。

注意,tx.send(val).unwrap(); 之后,val的所有權(quán)已經(jīng)發(fā)生了變化,接下來在子線程中不能再對val進行操作,否則會有編譯錯誤,如下代碼:

  1. use std::thread; 
  2.  
  3. use std::sync::mpsc; 
  4.  
  5. fn main() { 
  6.  
  7. let (tx, rx) = mpsc::channel(); 
  8.  
  9. thread::spawn(move || { 
  10.  
  11. let val = String::from("hi"); 
  12.  
  13. tx.send(val).unwrap(); 
  14.  
  15. println!("val is {}", val);//在這里會發(fā)生編譯錯誤 
  16.  
  17. }); 
  18.  
  19. let received = rx.recv().unwrap(); 
  20.  
  21. println!("Got: {}", received); 
  22.  

這里嘗試在通過 tx.send 發(fā)送 val 到通道中之后將其打印出來。允許這么做是一個壞主意:一旦將值發(fā)送到另一個線程后,那個線程可能會在我們再次使用它之前就將其修改或者丟棄。這會由于不一致或不存在的數(shù)據(jù)而導(dǎo)致錯誤或意外的結(jié)果。對于上面的代碼,編譯器給出錯誤:

  1. error[E0382]: use of moved value: `val` 
  2.  
  3. --> src/main.rs:10:31 
  4.  
  5.  
  6. 9 | tx.send(val).unwrap(); 
  7.  
  8. | --- value moved here 
  9.  
  10. 10 | println!("val is {}", val); 
  11.  
  12. | ^^^ value used here after move 
  13.  
  14.  
  15. = note: move occurs because `val` has type `std::string::String`, which does 
  16.  
  17. not implement the `Copy` trait 

我們通過channel能夠?qū)崿F(xiàn)多線程發(fā)送共享數(shù)據(jù),但是依然有個問題:通道一旦將一個值或者對象send出去之后,我們將無法再使用這個值;如果面對這樣一個需求:將一個計數(shù)器counter傳給10條線程,每條線程對counter加1,最后在main函數(shù)中匯總打印出counter的值,這樣一個簡單的需求如果使用C++或者Golang或者其它非Rust語言實現(xiàn),非常容易,一個全局變量,一把鎖,幾行代碼輕松搞定,但是Rust語言可就沒那么簡單,如果你是一個新手,你可能會經(jīng)歷如下“艱難歷程”:

首先很自然寫出第一版:

  1. use std::sync::Mutex; 
  2.  
  3. use std::thread; 
  4.  
  5. fn main() { 
  6.  
  7. let counter = Mutex::new(0); 
  8.  
  9. let mut handles = vec![]; 
  10.  
  11. for _ in 0..10 { 
  12.  
  13. let handle = thread::spawn(move || { 
  14.  
  15. let mut num = counter.lock().unwrap(); 
  16.  
  17. *num += 1
  18.  
  19. }); 
  20.  
  21. handles.push(handle); 
  22.  
  23.  
  24. for handle in handles { 
  25.  
  26. handle.join().unwrap(); 
  27.  
  28.  
  29. println!("Result: {}", *counter.lock().unwrap()); 
  30.  

多線程有了,Mutex鎖也有了,能保證每一次加一都是原子操作,代碼看起來沒什么問題,但是編譯器會無情報錯:

  1. error[E0382]: capture of moved value: `counter` 
  2.  
  3. --> src/main.rs:10:27 
  4.  
  5.  
  6. 9 | let handle = thread::spawn(move || { 
  7.  
  8. | ------- value moved (into closure) here 
  9.  
  10. 10 | let mut num = counter.lock().unwrap(); 
  11.  
  12. | ^^^^^^^ value captured here after move 
  13.  
  14.  
  15. = note: move occurs because `counter` has type `std::sync::Mutex<i32>`, 
  16.  
  17. which does not implement the `Copy` trait 
  18.  
  19. error[E0382]: use of moved value: `counter` 
  20.  
  21. --> src/main.rs:21:29 
  22.  
  23.  
  24. 9 | let handle = thread::spawn(move || { 
  25.  
  26. | ------- value moved (into closure) here 
  27.  
  28. ... 
  29.  
  30. 21 | println!("Result: {}", *counter.lock().unwrap()); 
  31.  
  32. | ^^^^^^^ value used here after move 
  33.  
  34.  
  35. = note: move occurs because `counter` has type `std::sync::Mutex<i32>`, 
  36.  
  37. which does not implement the `Copy` trait 
  38.  
  39. error: aborting due to 2 previous errors 

錯誤信息表明 counter 值的所有權(quán)被move了,但是我們又去引用了,根據(jù)所有權(quán)規(guī)則,所有權(quán)轉(zhuǎn)移之后不允許訪問,但是為什么會發(fā)生?

讓我們簡化程序來進行分析。不同于在 for 循環(huán)中創(chuàng)建 10 個線程,僅僅創(chuàng)建兩個線程來觀察發(fā)生了什么。將示例中第一個 for 循環(huán)替換為如下代碼:

  1. let handle = thread::spawn(move || { 
  2.  
  3. let mut num = counter.lock().unwrap(); 
  4.  
  5. *num += 1
  6.  
  7. }); 
  8.  
  9. handles.push(handle); 
  10.  
  11. let handle2 = thread::spawn(move || { 
  12.  
  13. let mut num2 = counter.lock().unwrap(); 
  14.  
  15. *num2 += 1
  16.  
  17. }); 
  18.  
  19. handles.push(handle2); 

這里創(chuàng)建了兩個線程并將用于第二個線程的變量名改為 handle2 和 num2,編譯會給出如下錯誤:

  1. error[E0382]: capture of moved value: `counter` 
  2.  
  3. --> src/main.rs:16:24 
  4.  
  5.  
  6. 8 | let handle = thread::spawn(move || { 
  7.  
  8. | ------- value moved (into closure) here 
  9.  
  10. ... 
  11.  
  12. 16 | let mut num2 = counter.lock().unwrap(); 
  13.  
  14. | ^^^^^^^ value captured here after move 
  15.  
  16.  
  17. = note: move occurs because `counter` has type `std::sync::Mutex<i32>`, 
  18.  
  19. which does not implement the `Copy` trait 
  20.  
  21. error[E0382]: use of moved value: `counter` 
  22.  
  23. --> src/main.rs:26:29 
  24.  
  25.  
  26. 8 | let handle = thread::spawn(move || { 
  27.  
  28. | ------- value moved (into closure) here 
  29.  
  30. ... 
  31.  
  32. 26 | println!("Result: {}", *counter.lock().unwrap()); 
  33.  
  34. | ^^^^^^^ value used here after move 
  35.  
  36.  
  37. = note: move occurs because `counter` has type `std::sync::Mutex<i32>`, 
  38.  
  39. which does not implement the `Copy` trait 
  40.  
  41. error: aborting due to 2 previous errors 

啊哈!第一個錯誤信息中說,counter 所有權(quán)被移動進了 handle 所代表線程的閉包中。因此我們無法在第二個線程中再次捕獲 counter , Rust 告訴我們不能將 counter 的所有權(quán)移動到多個線程中。所以錯誤原因明朗了,因為我們在循環(huán)中創(chuàng)建了多個線程,第一條線程獲取了 counter 所有權(quán)后,后面的線程再也拿不到 counter 的所有權(quán)。如何讓多條線程同時間接(注意,只能是間接)擁有一個對象的所有權(quán),哦,對了,引用計數(shù)!

通過使用智能指針 Rc<T> 來創(chuàng)建引用計數(shù)的值,嘗試使用 Rc<T> 來允許多個線程擁有 Mutex<T> 于是寫了第二版:

  1. use std::rc::Rc; 
  2.  
  3. use std::sync::Mutex; 
  4.  
  5. use std::thread; 
  6.  
  7. fn main() { 
  8.  
  9. let counter = Rc::new(Mutex::new(0)); 
  10.  
  11. let mut handles = vec![]; 
  12.  
  13. for _ in 0..10 { 
  14.  
  15. let counter = Rc::clone(&counter); 
  16.  
  17. let handle = thread::spawn(move || { 
  18.  
  19. let mut num = counter.lock().unwrap(); 
  20.  
  21. *num += 1
  22.  
  23. }); 
  24.  
  25. handles.push(handle); 
  26.  
  27.  
  28. for handle in handles { 
  29.  
  30. handle.join().unwrap(); 
  31.  
  32.  
  33. println!("Result: {}", *counter.lock().unwrap()); 
  34.  

再一次編譯并…出現(xiàn)了不同的錯誤!編譯器真是教會了我們很多!

  1. error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>: 
  2.  
  3. std::marker::Send` is not satisfied in `[closure@src/main.rs:11:36
  4.  
  5. 15:10 
  6.  
  7. counter:std::rc::Rc<std::sync::Mutex<i32>>]` 
  8.  
  9. --> src/main.rs:11:22 
  10.  
  11.  
  12. 11 | let handle = thread::spawn(move || { 
  13.  
  14. | ^^^^^^^^^^^^^ `std::rc::Rc<std::sync::Mutex<i32>>` 
  15.  
  16. cannot be sent between threads safely 
  17.  
  18.  
  19. = help: within `[closure@src/main.rs:11:3615:10 
  20.  
  21. counter:std::rc::Rc<std::sync::Mutex<i32>>]`, the trait `std::marker::Send` is 
  22.  
  23. not implemented for `std::rc::Rc<std::sync::Mutex<i32>>` 
  24.  
  25. = note: required because it appears within the type 
  26.  
  27. `[closure@src/main.rs:11:3615:10 
  28.  
  29. counter:std::rc::Rc<std::sync::Mutex<i32>>]` 
  30.  
  31. = note: required by `std::thread::spawn` 

編譯錯誤信息中有關(guān)鍵的一句:

`std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely。

不幸的是,Rc<T> 并不能安全的在線程間共享。當 Rc<T> 管理引用計數(shù)時,它必須在每一個 clone 調(diào)用時增加計數(shù),并在每一個克隆被丟棄時減少計數(shù)。Rc<T> 并沒有使用任何并發(fā)原語,來確保改變計數(shù)的操作不會被其他線程打斷。在計數(shù)出錯時可能會導(dǎo)致詭異的 bug,比如可能會造成內(nèi)存泄漏,或在使用結(jié)束之前就丟棄一個值。我們所需要的是一個完全類似 Rc<T>,又以一種線程安全的方式改變引用計數(shù)的類型。所幸 Arc<T> 正是 這么一個類似 Rc<T> 并可以安全的用于并發(fā)環(huán)境的類型。字母 “a” 代表 原子性(atomic),所以這是一個原子引用計數(shù)(atomically reference counted)類型。

于是改寫了第三版:

  1. use std::sync::{Mutex, Arc}; 
  2.  
  3. use std::thread; 
  4.  
  5. fn main() { 
  6.  
  7. let counter = Arc::new(Mutex::new(0)); 
  8.  
  9. let mut handles = vec![]; 
  10.  
  11. for _ in 0..10 { 
  12.  
  13. let counter = Arc::clone(&counter); 
  14.  
  15. let handle = thread::spawn(move || { 
  16.  
  17. let mut num = counter.lock().unwrap(); 
  18.  
  19. *num += 1
  20.  
  21. }); 
  22.  
  23. handles.push(handle); 
  24.  
  25.  
  26. for handle in handles { 
  27.  
  28. handle.join().unwrap(); 
  29.  
  30.  
  31. println!("Result: {}", *counter.lock().unwrap()); 
  32.  

這次編譯通過,并且打印出了正確的結(jié)果,最終,在嚴厲的編譯器的逐步引導(dǎo),“諄諄教誨”下,我們總算寫出了正確的代碼。

Rust編譯器對多線程數(shù)據(jù)共享,多線程數(shù)據(jù)傳遞這種內(nèi)存安全事故多發(fā)區(qū)進行了極其嚴苛的檢查和限制,確保編譯時就能發(fā)現(xiàn)潛在的內(nèi)存安全問題。在多線程傳遞數(shù)據(jù)時,除了通過channel,你沒有第二種選擇;在多線程數(shù)據(jù)共享時,除了Arc+Mutex(如果多線程共享的只是int bool這類簡單數(shù)據(jù)類型,你還可以使用原子操作) ,你同樣沒有別的選擇。雖然 Rust極其缺乏靈活性,但是這同樣是它的有點,因為編譯器一直在逼著你寫出正確的代碼,極大減少了程序的維護成本。

以上是我對Rust內(nèi)存安全保障手段的一些理解,Rust使用一些乍一看很奇怪的特性,非常清晰的定義了一個安全的邊界,并在上面做以足夠的檢查,保證你的代碼不會出問題。Rust做到了沒有垃圾回收的內(nèi)存安全,沒有數(shù)據(jù)競爭的并發(fā)安全。同時一個新手Rust程序員剛?cè)肟覴ust時,大部分的時間都是在解決編譯問題。一個新手C++程序員初期可能會寫出很多不安全的代碼,埋下很多坑,但是新手Rust不會,因為一個新手Rust寫出的不安全代碼在編譯階段就被攔截了,根本沒有機會埋坑,Rust承諾編譯通過的Rust程序不會存在內(nèi)存安全問題(注意:如果通過unsafe關(guān)鍵字強制關(guān)閉安全檢查,則依然有可能出現(xiàn)內(nèi)存安全問題)。

三 Rust開發(fā)效率問題

關(guān)于Rust開發(fā)效率問題,沒有一個統(tǒng)一的客觀評價標準,基本靠個人主觀感覺而定。每個人對不同語言掌握的熟練度也是影響開發(fā)效率的重要因素。關(guān)于開發(fā)效率,談一談個人的感受:先說入門,由于Rust一些奇葩的語法的存在(最麻煩的莫過于生命周期標記),導(dǎo)致Rust入門不像Python和Golang等語言那樣輕松,但是因為Rust主要是為了替代C/C++這類系統(tǒng)語言而存在,其借鑒了大量C++的語法,如果對C++熟悉,Rust入門不是難事;其次說說開發(fā)速度,對于初學(xué)者,Rust開發(fā)體驗就像在上海開始實行的垃圾分類時上海人民的那種困惑和凌亂,編譯器檢查太嚴格了,大多數(shù)時間都是在解決編譯問題,一種在其它語言中理所當然的寫法,在Rust中就是不行,不過好在編譯器的提示非常友好,根據(jù)編譯錯誤提示大多數(shù)時候能夠找到答案,不過編譯雖然費事,可一旦編譯通過,程序員就不需要關(guān)心內(nèi)存安全,內(nèi)存泄漏等頭疼問題,只需要關(guān)注于業(yè)務(wù)邏輯,寫了一個多月的Rust,debug次數(shù)屈指可數(shù),而且每次debug都是因為業(yè)務(wù)邏輯,從來沒有因為代碼內(nèi)存錯誤,崩潰等問題debug;如果對Rust稍微熟練一些,其開發(fā)速度絕對不會比Python和Golang慢,因為在編譯階段,Rust就解決了大部分的問題,省去了大量的debug時間。

四 跨平臺性

Rust跨平臺性和Golang一樣,擁有優(yōu)秀的跨平臺性,支持交叉編譯,一份代碼可編譯出支持windows、 linux、arm、macos、freebsd等平臺上運行的二進制,且完全靜態(tài)編譯,運行時不依賴任何第三方庫。這個特性對于飽受C++跨平臺編譯折磨的程序員來說簡直是福音。Rust對嵌入式環(huán)境同樣支持友好,有人用Rust寫了一個簡單的操作系統(tǒng)[6]。

五 生態(tài)問題

這一方面應(yīng)該是Rust最弱的地方,作為一個后起之秀,其生態(tài)遠遠不如Python和Golang豐富,不過使用率很高的一些常用庫都能找到;并且Rust連續(xù)3年成為Stack Overflow最受歡迎的語言[7],受到的關(guān)注度越來越高[8],相信未來Rust的社區(qū)一定會越來越豐富。

最后靈魂一問收尾:

沒有垃圾回收的內(nèi)存安全,沒有數(shù)據(jù)競爭的并發(fā)安全、資源消耗低而性能強勁、開發(fā)效率高并且跨平臺性優(yōu)良,這樣的Rust香不香?要不要擁抱一個?

 

 

責任編輯:張燕妮 來源: 阿里技術(shù)
相關(guān)推薦

2022-03-25 09:57:18

C++Rust語言

2014-02-21 09:18:18

2014-01-02 14:24:29

火狐擴展火狐瀏覽器

2022-03-30 12:06:15

云桌面

2017-03-23 15:15:09

開發(fā)人工智能機器學(xué)習

2023-01-09 17:10:54

2024-07-30 11:17:54

2010-02-06 15:41:08

ibmdwJava

2024-02-19 09:02:00

Rust編碼

2024-09-03 10:01:21

2010-02-24 15:27:17

ibmdw

2017-06-13 13:29:32

前端框架

2018-07-02 10:46:18

2012-01-04 10:08:42

IE8瀏覽器

2012-01-09 16:58:29

移動客戶端

2010-12-20 10:27:54

2024-07-30 11:55:15

2010-02-06 15:43:46

ibmdwWeb開發(fā)

2013-07-22 15:38:08

Java大師底層

2022-03-15 08:41:57

Firefox擴展瀏覽器
點贊
收藏

51CTO技術(shù)棧公眾號