連續(xù) 3 年最受歡迎:Rust,香!
我們在選擇一種開發(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)的變量。
例如:
- let obj = String::from("hello");
obj是String對象的所有權(quán)變量。
2)值或?qū)ο笥星抑荒苡幸粋€所有者。
3)當所有者離開作用域,所有者所代表的對象或者值會被立即銷毀。
4)賦值語句、函數(shù)調(diào)用、函數(shù)返回等會導(dǎo)致所有權(quán)轉(zhuǎn)移,原有變量會失效。
例如:
- fn main() {
- let s = String::from("hello");
- let s1 = s; //所有權(quán)發(fā)生了轉(zhuǎn)移,由s轉(zhuǎn)移給s1
- print!("{}",s); //s無效,不能訪問,此句編譯會報錯
- }
- fn test(s1:String){
- print!("{}",s1);
- }
- fn main() {
- let s = String::from("hello");
- test(s); //傳參,所有權(quán)發(fā)生了轉(zhuǎn)移
- print!("{}",s); //此處s無效,編譯報錯
- }
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會在編譯時檢查所有借出的值,確保它們的壽命不會超過值本身的壽命。
例如,以下的寫法就沒有問題:
- fn test(s1:&String){
- print!("{}",s1);
- }
- fn main() {
- let s = String::from("hello");
- test(&s); //傳參,注意只是傳遞了引用,所有權(quán)還歸屬于s
- print!("{}",s); //此處s依然有效,可以訪問
- }
- fn main() {
- let s = String::from("hello");
- let s1 = &s; //s1借用s,所有權(quán)還歸屬于s
- print!("{}",s); //此處s依然有效,可以訪問
- print!("{}",s1); //此處s1和s指向同一個對象
- }
如果我們嘗試修改借用的變量呢?
- fn main() {
- let s = String::from("hello");
- change(&s);
- }
- fn change(some_string: &String) {
- some_string.push_str(", world");
- }
借用默認是不可變的,上面的代碼編譯時會報錯:
- error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
- --> error.rs:8:5
- |
- 7 | fn change(some_string: &String) {
- | ------- use `&mut String` here to make mutable
- 8 | some_string.push_str(", world");
- | ^^^^^^^^^^^ cannot borrow as mutable
根據(jù)編譯錯誤的提示,通過mut關(guān)鍵字將默認借用修改為可變借用就OK,如下代碼可以編譯通過:
- fn main() {
- let mut s = String::from("hello");
- change(&mut s);
- }
- fn change(some_string: &mut String) {
- some_string.push_str(", world");
- }
不過可變引用有一個很大的限制:在特定作用域中的特定數(shù)據(jù)有且只能有一個可變引用,這個限制的好處是 Rust 可以在編譯時就避免數(shù)據(jù)競爭,這些代碼會失敗:
- let mut s = String::from("hello");
- let r1 = &mut s;
- let r2 = &mut s;
報錯如下:
- error[E0499]: cannot borrow `s` as mutable more than once at a time
- --> borrow_twice.rs:5:19
- |
- 4 | let r1 = &mut s;
- | - first mutable borrow occurs here
- 5 | let r2 = &mut s;
- | ^ second mutable borrow occurs here
- 6 | }
- | - first borrow ends here
在存在指針的語言中,容易通過釋放內(nèi)存時保留指向它的指針而錯誤地生成一個 懸垂指針(dangling pointer),所謂懸垂指針是其指向的內(nèi)存可能已經(jīng)被分配給其它持有者或者已經(jīng)被釋放。相比之下,在 Rust 中編譯器確保引用永遠也不會變成懸垂狀態(tài):當我們擁有一些數(shù)據(jù)的引用,編譯器確保數(shù)據(jù)不會在其引用之前離開作用域。
讓我們嘗試創(chuàng)建一個懸垂引用,Rust 會通過一個編譯時錯誤來避免:
- fn main() {
- let reference_to_nothing = dangle();
- }
- fn dangle() -> &String {
- let s = String::from("hello");
- &s
- }
這里是編譯錯誤:
- error[E0106]: missing lifetime specifier
- --> dangle.rs:5:16
- |
- 5 | fn dangle() -> &String {
- | ^ expected lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but there is
- no value for it to be borrowed from
- = 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 的值:
- error[E0106]: missing lifetime specifier
- --> dangle.rs:5:16
- |
- 5 | fn dangle() -> &String {
- | ^ expected lifetime parameter
- |
- = help: this function's return type contains a borrowed value, but there is
- no value for it to be borrowed from
- = help: consider giving it a 'static lifetime
當編譯這段代碼時會得到一個錯誤:
- error: `x` does not live long enough
- |
- 6 | r = &x;
- | - borrow occurs here
- 7 | }
- | ^ `x` dropped here while still borrowed
- ...
- 10 | }
- | - borrowed value needs to live until here
編譯錯誤顯示:變量 x 并沒有 “活的足夠久”,那么Rust是如何判斷的呢?
編譯器的這一部分叫做 借用檢查器(borrow checker),它比較作用域來確保所有的借用都是有效的。如下:r 和 x 的生命周期注解,分別叫做 'a 和 'b:
- {
- let r; // -------+-- 'a
- // |
- { // |
- let x = 5; // -+-----+-- 'b
- r = &x; // | |
- } // -+ |
- // |
- println!("r: {}", r); // |
- } // -------+
我們將 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:
- use std::thread;
- fn main() {
- let v = vec![1, 2, 3];
- let handle = thread::spawn(|| {
- println!("Here's a vector: {:?}", v);
- });
- handle.join().unwrap();
- }
閉包使用了 v,所以閉包會捕獲 v 并使其成為閉包環(huán)境的一部分。因為 thread::spawn 在一個新線程中運行這個閉包,所以可以在新線程中訪問 v。然而當編譯這個例子時,會得到如下錯誤:
- error[E0373]: closure may outlive the current function, but it borrows `v`,
- which is owned by the current function
- --> src/main.rs:6:32
- |
- 6 | let handle = thread::spawn(|| {
- | ^^ may outlive borrowed value `v`
- 7 | println!("Here's a vector: {:?}", v);
- | - `v` is borrowed here
- |
- help: to force the closure to take ownership of `v` (and any other referenced
- variables), use the `move` keyword
- |
- 6 | let handle = thread::spawn(move || {
- | ^^^^^^^
Rust 會“推斷”如何捕獲 v,因為 println! 只需要 v 的引用,閉包嘗試借用 v。然而這有一個問題:Rust 不知道這個新建線程會執(zhí)行多久,所以無法知曉 v 的引用是否一直有效。所以編譯器提示:
closure may outlive the current function, but it borrows `v` 。
下面展示了一個 v 的引用很有可能不再有效的場景:
- use std::thread;
- fn main() {
- let v = vec![1, 2, 3];
- let handle = thread::spawn(|| {
- println!("Here's a vector: {:?}", v);
- });
- drop(v); // 強制釋放變量v
- handle.join().unwrap();
- }
為了修復(fù)示上面的編譯錯誤,我們可以聽取編譯器的建議:
- help: to force the closure to take ownership of `v` (and any other referenced
- variables), use the `move` keyword
- |
- 6 | let handle = thread::spawn(move || {
接下來是正確的寫法:
- use std::thread;
- fn main() {
- let v = vec![1, 2, 3];
- let handle = thread::spawn(move || { //使用 move 關(guān)鍵字強制獲取它使用的值的所有權(quán),接下來就可以正常使用v了
- println!("Here's a vector: {:?}", v);
- });
- handle.join().unwrap();
- }
從上面簡單例子中可以看出多線程間參數(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的通道,用法類似。
示例:
- use std::thread;
- use std::sync::mpsc;
- fn main() {
- let (tx, rx) = mpsc::channel();
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- });
- let received = rx.recv().unwrap();
- println!("Got: {}", received);
- }
上例中,我們可以在main函數(shù)中通過channel得到了子線程中的對象val。
注意,tx.send(val).unwrap(); 之后,val的所有權(quán)已經(jīng)發(fā)生了變化,接下來在子線程中不能再對val進行操作,否則會有編譯錯誤,如下代碼:
- use std::thread;
- use std::sync::mpsc;
- fn main() {
- let (tx, rx) = mpsc::channel();
- thread::spawn(move || {
- let val = String::from("hi");
- tx.send(val).unwrap();
- println!("val is {}", val);//在這里會發(fā)生編譯錯誤
- });
- let received = rx.recv().unwrap();
- println!("Got: {}", received);
- }
這里嘗試在通過 tx.send 發(fā)送 val 到通道中之后將其打印出來。允許這么做是一個壞主意:一旦將值發(fā)送到另一個線程后,那個線程可能會在我們再次使用它之前就將其修改或者丟棄。這會由于不一致或不存在的數(shù)據(jù)而導(dǎo)致錯誤或意外的結(jié)果。對于上面的代碼,編譯器給出錯誤:
- error[E0382]: use of moved value: `val`
- --> src/main.rs:10:31
- |
- 9 | tx.send(val).unwrap();
- | --- value moved here
- 10 | println!("val is {}", val);
- | ^^^ value used here after move
- |
- = note: move occurs because `val` has type `std::string::String`, which does
- 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)歷如下“艱難歷程”:
首先很自然寫出第一版:
- use std::sync::Mutex;
- use std::thread;
- fn main() {
- let counter = Mutex::new(0);
- let mut handles = vec![];
- for _ in 0..10 {
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
- *num += 1;
- });
- handles.push(handle);
- }
- for handle in handles {
- handle.join().unwrap();
- }
- println!("Result: {}", *counter.lock().unwrap());
- }
多線程有了,Mutex鎖也有了,能保證每一次加一都是原子操作,代碼看起來沒什么問題,但是編譯器會無情報錯:
- error[E0382]: capture of moved value: `counter`
- --> src/main.rs:10:27
- |
- 9 | let handle = thread::spawn(move || {
- | ------- value moved (into closure) here
- 10 | let mut num = counter.lock().unwrap();
- | ^^^^^^^ value captured here after move
- |
- = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
- which does not implement the `Copy` trait
- error[E0382]: use of moved value: `counter`
- --> src/main.rs:21:29
- |
- 9 | let handle = thread::spawn(move || {
- | ------- value moved (into closure) here
- ...
- 21 | println!("Result: {}", *counter.lock().unwrap());
- | ^^^^^^^ value used here after move
- |
- = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
- which does not implement the `Copy` trait
- 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)替換為如下代碼:
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
- *num += 1;
- });
- handles.push(handle);
- let handle2 = thread::spawn(move || {
- let mut num2 = counter.lock().unwrap();
- *num2 += 1;
- });
- handles.push(handle2);
這里創(chuàng)建了兩個線程并將用于第二個線程的變量名改為 handle2 和 num2,編譯會給出如下錯誤:
- error[E0382]: capture of moved value: `counter`
- --> src/main.rs:16:24
- |
- 8 | let handle = thread::spawn(move || {
- | ------- value moved (into closure) here
- ...
- 16 | let mut num2 = counter.lock().unwrap();
- | ^^^^^^^ value captured here after move
- |
- = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
- which does not implement the `Copy` trait
- error[E0382]: use of moved value: `counter`
- --> src/main.rs:26:29
- |
- 8 | let handle = thread::spawn(move || {
- | ------- value moved (into closure) here
- ...
- 26 | println!("Result: {}", *counter.lock().unwrap());
- | ^^^^^^^ value used here after move
- |
- = note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
- which does not implement the `Copy` trait
- 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> 于是寫了第二版:
- use std::rc::Rc;
- use std::sync::Mutex;
- use std::thread;
- fn main() {
- let counter = Rc::new(Mutex::new(0));
- let mut handles = vec![];
- for _ in 0..10 {
- let counter = Rc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
- *num += 1;
- });
- handles.push(handle);
- }
- for handle in handles {
- handle.join().unwrap();
- }
- println!("Result: {}", *counter.lock().unwrap());
- }
再一次編譯并…出現(xiàn)了不同的錯誤!編譯器真是教會了我們很多!
- error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
- std::marker::Send` is not satisfied in `[closure@src/main.rs:11:36:
- 15:10
- counter:std::rc::Rc<std::sync::Mutex<i32>>]`
- --> src/main.rs:11:22
- |
- 11 | let handle = thread::spawn(move || {
- | ^^^^^^^^^^^^^ `std::rc::Rc<std::sync::Mutex<i32>>`
- cannot be sent between threads safely
- |
- = help: within `[closure@src/main.rs:11:36: 15:10
- counter:std::rc::Rc<std::sync::Mutex<i32>>]`, the trait `std::marker::Send` is
- not implemented for `std::rc::Rc<std::sync::Mutex<i32>>`
- = note: required because it appears within the type
- `[closure@src/main.rs:11:36: 15:10
- counter:std::rc::Rc<std::sync::Mutex<i32>>]`
- = 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)類型。
于是改寫了第三版:
- use std::sync::{Mutex, Arc};
- use std::thread;
- fn main() {
- let counter = Arc::new(Mutex::new(0));
- let mut handles = vec![];
- for _ in 0..10 {
- let counter = Arc::clone(&counter);
- let handle = thread::spawn(move || {
- let mut num = counter.lock().unwrap();
- *num += 1;
- });
- handles.push(handle);
- }
- for handle in handles {
- handle.join().unwrap();
- }
- println!("Result: {}", *counter.lock().unwrap());
- }
這次編譯通過,并且打印出了正確的結(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香不香?要不要擁抱一個?