我最近向Rust的文檔提交了一個提案。 我通篇提案中一個重要組成部分是對可能據說過Rust的人簡短而簡單的介紹,以便他們可以肯定Rust是否適合他們。 前幾天,我看到了一個精彩的演講,並認爲它能夠做爲這個介紹的一個很好的基礎。 將此視爲此類介紹的RFC(Request For Comments)。 很是歡迎反饋在rust-dev或Twitter上。html
Rust是一種系統編程語言,專一於強大的編譯時正確性保證。 它經過提供很是強大的編譯時保證和對內存生命週期的明確控制,改進了其餘系統語言(如C ++,D和Cyclone)的思想。 強大的內存保證使編寫正確的併發Rust代碼比使用其餘語言更容易。 這可能聽起來很是複雜,但它比聽起來更容易! 本教程將讓您在大約30分鐘內瞭解Rust。 但願你至少模糊地熟悉之前的「大括號」語言。 這些概念比語法更重要,因此若是你沒有獲得每個細節,請不要擔憂:本教程能夠幫助你解決這個問題。程序員
讓咱們來談談Rust中最重要的概念:「全部權」,以及它對併發編程(對程序員來說一般是很是困難的任務)的啓發。編程
全部權是Rust的核心,也是其更有趣和獨特的功能之一。 「全部權」是指容許哪部分的代碼修改內存。 讓咱們從查看一些C ++代碼開始:安全
int *dangling(void) { int i = 1234; return &i; } int add_one(void) { int *num = dangling(); return *num + 1; }
dangling函數在棧上分配了一個整型,而後保存給一個變量i,最後返回了這個變量i的引用。這裏有一個問題:當函數返回時棧內存變成失效。意味着在函數add_one第二行,指針num指向了垃圾值,咱們將沒法獲得想要的結果。雖然這個一個簡單的例子,可是在C++的代碼裏會常常發生。當堆上的內存使用malloc(或new)分配,而後使用free(或delete)釋放時,會出現相似的問題,可是您的代碼會嘗試使用指向該內存的指針執行某些操做。 更現代的C ++使用RAII和構造函數/析構函數,但它們沒法徹底避免「懸空指針」。 這個問題被稱爲「懸空指針」,而且不可能編寫出現「懸空指針」的Rust代碼。 咱們試試吧:閉包
fn dangling() -> &int { let i = 1234; return &i; } fn add_one() -> int { let num = dangling(); return *num + 1; }
當你嘗試編譯這個程序時,你會獲得一個有趣和很是長的錯誤信息:併發
temp.rs:3:11: 3:13 error: borrowed value does not live long enough temp.rs:3 return &i; temp.rs:1:22: 4:1 note: borrowed pointer must be valid for the anonymous lifetime #1 defined on the block at 1:22... temp.rs:1 fn dangling() -> &int { temp.rs:2 let i = 1234; temp.rs:3 return &i; temp.rs:4 } temp.rs:1:22: 4:1 note: ...but borrowed value is only valid for the block at 1:22 temp.rs:1 fn dangling() -> &int { temp.rs:2 let i = 1234; temp.rs:3 return &i; temp.rs:4 } error: aborting due to previous error
爲了徹底理解這個錯誤信息,咱們須要談談「擁有」某些東西意味着什麼。 因此如今,讓咱們接受Rust不容許咱們用懸空指針編寫代碼,一旦咱們理解了全部權,咱們就會回來看這塊代碼。編程語言
讓咱們先放下編程一下子,先聊聊書籍。 我喜歡讀實體書,有時候我真的很喜歡一本書,並告訴個人朋友他們應該閱讀它。 當我讀個人書時,我擁有它:這本書是我所擁有的。 當我把書借給別人一段時間,他們向我「借用」這本書。 當你借用一本書時,在特定的一段時間它是屬於你的,而後你把它還給我,我又擁有它了。 對嗎?函數
這個概念也直接應用於Rust代碼:一些代碼「擁有」一個指向內存的特定指針。 它是該指針的惟一全部者。 它還能夠暫時將該內存借給其餘代碼:代碼「借用」它。 借用它一段時間,稱爲「生命週期」。工具
這是關於全部權的全部。 那彷佛並不那麼難,對吧? 讓咱們回到那條錯誤信息:error: borrowed value does not live long enough。 咱們試圖使用Rust的借用指針&,借出一個特定的變量i。 但Rust知道函數返回後該變量無效,所以它告訴咱們:性能
borrowed pointer must be valid for the anonymous lifetime #1 ... but borrowed value is only valid for the block。
優美!
這是棧內存的一個很好的例子,但堆內存呢? Rust有第二種指針,一個'惟一'指針,你能夠用〜建立。 看看這個:
fn dangling() -> ~int { let i = ~1234; return i; } fn add_one() -> int { let num = dangling(); return *num + 1; }
此代碼將成功編譯。 請注意,咱們使用指針指向該值而不是將1234分配給棧:~1234。 你能夠大體比較這兩行:
// rust let i = ~1234; // C++ int *i = new int; *i = 1234;
Rust可以推斷出類型的大小,而後分配正確的內存大小並將其設置爲您要求的值。 這意味着沒法分配未初始化的內存:Rust沒有null的概念。萬歲! Rust和C ++之間還有另一個區別:Rust編譯器還計算了i的生命週期,而後在它無效後插入相應的free調用,就像C ++中的析構函數同樣。 您能夠得到手動分配堆內存的全部好處,而無需本身完成全部工做。 此外,全部這些檢查都是在編譯時完成的,所以沒有運行時開銷。 若是你編寫了正確的C ++代碼,你將編寫出與C++代碼基本上相同的Rust代碼。並且因爲編譯器的幫忙,編寫錯誤的代碼版本是不可能的。
你已經看到了一種狀況,全部權和生命週期有利於防止在不太嚴格的語言中一般會出現的危險代碼。如今讓咱們談談另外一種狀況:併發。
併發是當前軟件世界中一個使人難以置信的熱門話題。 對於計算機科學家來講,它一直是一個有趣的研究領域,但隨着互聯網的使用爆炸式增加,人們正在尋求改善給定的服務能夠處理的用戶數量。 併發是實現這一目標的一種方式。 但併發代碼有一個很大的缺點:它很難推理,由於它是非肯定性的。 編寫好的併發代碼有幾種不一樣的方法,但讓咱們來談談Rust的全部權和生命週期的概念如何幫助實現正確而且併發的代碼。
首先,讓咱們回顧一下Rust中的簡單併發示例。 Rust容許你啓動task,這是輕量級的「綠色」線程。 這些任務沒有任何共享內存,所以,咱們使用「通道」在task之間進行通訊。 像這樣:
fn main() { let numbers = [1,2,3]; let (port, chan) = Chan::new(); chan.send(numbers); do spawn { let numbers = port.recv(); println!("{:d}", numbers[0]); } }
在這個例子中,咱們建立了一個數字的vector。 而後咱們建立一個新的Chan,這是Rust實現通道的包名。 這將返回通道的兩個不一樣端:通道(channel)和端口(port)。 您將數據發送到通道端(channel),它從端口端(port)讀出。 spawn函數能夠啓動一個task。 正如你在代碼中看到的那樣,咱們在task中調用port.recv(),咱們在外面調用chan.send(),傳入vector。 而後打印vector的第一個元素。
這樣作是由於Rust在經過channel發送時copy了vector。 這樣,若是它是可變的,就不會有競爭條件。 可是,若是咱們正在啓動不少task,或者咱們的數據很是龐大,那麼爲每一個任務都copy副本會使咱們的內存使用量膨脹而沒有任何實際好處。
引入Arc。 Arc表明「原子引用計數」,它是一種在多個task之間共享不可變數據的方法。 這是一些代碼:
extern mod extra; use extra::arc::Arc; fn main() { let numbers = [1,2,3]; let numbers_arc = Arc::new(numbers); for num in range(0, 3) { let (port, chan) = Chan::new(); chan.send(numbers_arc.clone()); do spawn { let local_arc = port.recv(); let task_numbers = local_arc.get(); println!("{:d}", task_numbers[num]); } } }
這與咱們以前的代碼很是類似,除了如今咱們循環三次,啓動三個task,並在它們之間發送一個Arc。 Arc :: new建立一個新的Arc,.clone()返回Arc的新的引用,而.get()從Arc中獲取該值。 所以,咱們爲每一個task建立一個新的引用,將該引用發送到通道,而後使用引用打印出一個數字。 如今咱們不copy vector。
Arcs很是適合不可變數據,但可變數據呢? 共享可變狀態是併發程序的禍根。 您可使用互斥鎖(mutex)來保護共享的可變狀態,可是若是您忘記獲取互斥鎖(mutex),則可能會發生錯誤。
Rust爲共享可變狀態提供了一個工具:RWArc。 Arc的這個變種容許Arc的內容發生變異。 看看這個:
extern mod extra; use extra::arc::RWArc; fn main() { let numbers = [1,2,3]; let numbers_arc = RWArc::new(numbers); for num in range(0, 3) { let (port, chan) = Chan::new(); chan.send(numbers_arc.clone()); do spawn { let local_arc = port.recv(); local_arc.write(|nums| { nums[num] += 1 }); local_arc.read(|nums| { println!("{:d}", nums[num]); }) } } }
咱們如今使用RWArc包來獲取讀/寫Arc。 RWArc的API與Arc略有不一樣:讀和寫容許您讀取和寫入數據。 它們都將閉包做爲參數,而且在寫入的狀況下,RWArc將獲取互斥鎖,而後將數據傳遞給此閉包。 閉包完成後,互斥鎖被釋放。
你能夠看到在不記得獲取鎖的狀況下是不可能改變狀態的。 咱們得到了共享可變狀態的便利,同時保持不容許共享可變狀態的安全性。
但等等,這怎麼可能? 咱們不能同時容許和禁止可變狀態。 是什麼賦予了這種能力的?
所以,Rust語言不容許共享可變狀態,但我剛剛向您展現了一些容許共享可變狀態的代碼。 這怎麼可能? 答案:unsafe
你看,雖然Rust編譯器很是聰明,而且能夠避免你一般犯的錯誤,但它不是人工智能。 由於咱們比編譯器更聰明,有時候,咱們須要克服這種安全行爲。 爲此,Rust有一個unsafe關鍵字。 在一個unsafe的代碼塊裏,Rust關閉了許多安全檢查。 若是您的程序出現問題,您只須要審覈您在不安全範圍內所作的事情,而不是整個程序。
若是Rust的主要目標之一是安全,爲何要關閉安全? 嗯,實際上只有三個主要緣由:與外部代碼鏈接,例如將FFI寫入C庫,性能(在某些狀況下),以及圍繞一般不安全的操做提供安全抽象。 咱們的Arcs是最後一個目的的一個例子。 咱們能夠安全地分發對Arc的多個引用,由於咱們確信數據是不可變的,所以能夠安全地共享。 咱們能夠分發對RWArc的多個引用,由於咱們知道咱們已經將數據包裝在互斥鎖中,所以能夠安全地共享。 但Rust編譯器沒法知道咱們已經作出了這些選擇,因此在Arcs的實現中,咱們使用不安全的塊來作(一般)危險的事情。 可是咱們暴露了一個安全的接口,這意味着Arcs不可能被錯誤地使用。
這就是Rust的類型系統如何讓你不會犯一些使併發編程變得困難的錯誤,同時也能得到像C ++等語言同樣的效率。
我但願這個對Rust的嘗試能讓您瞭解Rust是否適合您。 若是這是真的,我建議您查看完整的教程,以便對Rust的語法和概念進行全面,深刻的探索。