并發(fā)是什么?引用Rob Pike的經(jīng)典描述:
并發(fā)是同一時(shí)間應(yīng)對(duì)多件事情的能力
其實(shí)在我們身邊就有很多并發(fā)的事情,比如一邊上課,一邊發(fā)短信;一邊給小孩喂奶,一邊看電視,只要你細(xì)心留意,就會(huì)發(fā)現(xiàn)許多類似的事。相應(yīng)地,在軟件的世界里,我們也會(huì)發(fā)現(xiàn)這樣的事,比如一邊寫博客,一邊聽音樂;一邊看網(wǎng)頁(yè),一邊下載軟件等等。顯而易見這樣會(huì)節(jié)約不少時(shí)間,干更多的事。然而一開始計(jì)算機(jī)系統(tǒng)并不能同時(shí)處理兩件事,這明顯滿足不了我們的需要,后來(lái)慢慢提出了多進(jìn)程,多線程的解決方案,再后來(lái),硬件也發(fā)展到了多核多CPU的地步。在硬件和系統(tǒng)底層對(duì)并發(fā)的支持也來(lái)越多,相應(yīng)地,各大編程語(yǔ)言也對(duì)并發(fā)處理提供了強(qiáng)力的支持,作為新興語(yǔ)言的Rust,自然也支持并發(fā)編程。那么本章就將引領(lǐng)大家一覽Rust并發(fā)編程的相關(guān)知識(shí),從線程開始,逐步嘗試進(jìn)行數(shù)據(jù)交互,同步協(xié)作,最后進(jìn)入到并行實(shí)現(xiàn),一步一步揭開Rust并發(fā)編程的神秘面紗。由于本書主要介紹的是Rust語(yǔ)言的使用,所以本章不會(huì)對(duì)并發(fā)編程相關(guān)理論知識(shí)進(jìn)行全面而深入地探討——要真那樣地話,一本書都不夠介紹的,而是更側(cè)重于介紹用Rust語(yǔ)言怎么實(shí)現(xiàn)基本的并發(fā)。
首先我們會(huì)介紹線程的使用,線程是基本的執(zhí)行單元,其重要性不言而喻,Rust程序就是由一堆線程組成的。在當(dāng)今多核多CPU已經(jīng)普及的情況下,各種大數(shù)據(jù)分析和并行計(jì)算又讓線程煥發(fā)出了更耀眼的光芒。如果對(duì)線程不甚了解,請(qǐng)先參閱操作系統(tǒng)相關(guān)的書籍,此處不過(guò)多介紹。然后介紹一些在解決并發(fā)問題時(shí),需要處理的數(shù)據(jù)傳遞和協(xié)作的實(shí)現(xiàn),比如消息傳遞,同步和共享內(nèi)存。最后簡(jiǎn)要介紹Rust中并行的實(shí)現(xiàn)。
相信線程對(duì)大家而言,一點(diǎn)也不陌生,在當(dāng)今多CPU多核已經(jīng)普及的情況下,大數(shù)據(jù)分析與并行計(jì)算都離不開它,幾乎所有的語(yǔ)言都支持它,所有的進(jìn)程都是由一個(gè)或多個(gè)線程所組成的。既然如此重要,接下來(lái)我們就先來(lái)看一下在Rust中如何創(chuàng)建一個(gè)線程,然后線程又是如何結(jié)束的。
Rust對(duì)于線程的支持,和C++11
一樣,都是放在標(biāo)準(zhǔn)庫(kù)中來(lái)實(shí)現(xiàn)的,詳情請(qǐng)參見std::thread
,好在Rust從一開始就這樣做了,不用像C++那樣等呀等。在語(yǔ)言層面支持后,開發(fā)者就不用那么苦兮兮地處理各平臺(tái)的移植問題。通過(guò)Rust的源碼可以看到,std::thread
其實(shí)就是對(duì)不同平臺(tái)的線程操作的封裝,相關(guān)API的實(shí)現(xiàn)都是調(diào)用操作系統(tǒng)的API來(lái)實(shí)現(xiàn)的,從而提供了線程操作的統(tǒng)一接口。對(duì)于我而言,能夠這樣簡(jiǎn)單快捷地操作原生線程,身上的壓力一下輕了不少。
首先,我們看一下在Rust中如何創(chuàng)建一個(gè)原生線程(native thread)。std::thread
提供了兩種創(chuàng)建方式,都非常簡(jiǎn)單,第一種方式是通過(guò)spawn
函數(shù)來(lái)創(chuàng)建,參見下面的示例代碼:
use std::thread;
fn main() {
// 創(chuàng)建一個(gè)線程
let new_thread = thread::spawn(move || {
println!("I am a new thread.");
});
// 等待新建線程執(zhí)行完成
new_thread.join().unwrap();
}
執(zhí)行上面這段代碼,將會(huì)看到下面的輸出結(jié)果:
I am a new thread.
就5行代碼,少得不能再少,最關(guān)鍵的當(dāng)然就是調(diào)用spawn
函數(shù)的那行代碼。使用這個(gè)函數(shù),記得要先use std::thread
。注意spawn
函數(shù)需要一個(gè)函數(shù)作為參數(shù),且是FnOnce
類型,如果已經(jīng)忘了這種類型的函數(shù),請(qǐng)學(xué)習(xí)或回顧一下函數(shù)和閉包章節(jié)。main
函數(shù)最后一行代碼即使不要,也能創(chuàng)建線程(關(guān)于join
函數(shù)的作用和使用在后續(xù)小節(jié)詳解,此處你只要知道它可以用來(lái)等待線程執(zhí)行完成即可),可以去掉或者注釋該行代碼試試。這樣的話,運(yùn)行結(jié)果可能沒有任何輸出,具體原因后面詳解。
接下來(lái)我們使用第二種方式創(chuàng)建線程,它比第一種方式稍微復(fù)雜一點(diǎn),因?yàn)楣δ軓?qiáng)大一點(diǎn),可以在創(chuàng)建之前設(shè)置線程的名稱和堆棧大小,參見下面的代碼:
use std::thread;
fn main() {
// 創(chuàng)建一個(gè)線程,線程名稱為 thread1, 堆棧大小為4k
let new_thread_result = thread::Builder::new()
.name("thread1".to_string())
.stack_size(4*1024*1024).spawn(move || {
println!("I am thread1.");
});
// 等待新創(chuàng)建的線程執(zhí)行完成
new_thread_result.unwrap().join().unwrap();
}
執(zhí)行上面這段代碼,將會(huì)看到下面的輸出結(jié)果:
I am thread1.
通過(guò)和第一種方式的實(shí)現(xiàn)代碼比較可以發(fā)現(xiàn),這種方式借助了一個(gè)Builder
類來(lái)設(shè)置線程名稱和堆棧大小,除此之外,Builder
的spawn
函數(shù)的返回值是一個(gè)Result
,在正式的代碼編寫中,可不能像上面這樣直接unwrap.join
,應(yīng)該判定一下。后面也會(huì)有很多類似的演示代碼,為了簡(jiǎn)單說(shuō)明不會(huì)做的很嚴(yán)謹(jǐn)。
以上就是Rust創(chuàng)建原生線程的兩種不同方式,示例代碼有點(diǎn)然并卵的意味,但是你可以稍加修改,就可以變得更加有用,試試吧。
此時(shí),我們已經(jīng)知道如何創(chuàng)建一個(gè)新線程了,創(chuàng)建后,不管你見或者不見,它就在那里,那么它什么時(shí)候才會(huì)消亡呢?自生自滅,亦或者被干掉?如果接觸過(guò)一些系統(tǒng)編程,應(yīng)該知道有些操作系統(tǒng)提供了粗暴地干掉線程的接口,看它不爽,直接干掉,完全可以不理會(huì)新建線程的感受。是否感覺很爽,但是Rust不會(huì)再讓這樣爽了,因?yàn)?code>std::thread并沒有提供這樣的接口,為什么呢?如果深入接觸過(guò)并發(fā)編程或多線程編程,就會(huì)知道強(qiáng)制終止一個(gè)運(yùn)行中的線程,會(huì)出現(xiàn)諸多問題。比如資源沒有釋放,引起狀態(tài)混亂,結(jié)果不可預(yù)期。強(qiáng)制干掉那一刻,貌似很爽地解決問題了,然而可能后患無(wú)窮。Rust語(yǔ)言的一大特性就是安全,是絕對(duì)不允許這樣不負(fù)責(zé)任的做法的。即使在其他語(yǔ)言提供了類似的接口,也不應(yīng)該濫用。
那么在Rust中,新建的線程就只能讓它自身自滅了嗎?其實(shí)也有兩種方式,首先介紹大家都知道的自生自滅的方式,線程執(zhí)行體執(zhí)行完成,線程就結(jié)束了。比如上面創(chuàng)建線程的第一種方式,代碼執(zhí)行完println!("I am a new thread.");
就結(jié)束了。 如果像下面這樣:
use std::thread;
fn main() {
// 創(chuàng)建一個(gè)線程
let new_thread = thread::spawn(move || {
loop {
println!("I am a new thread.");
}
});
// 等待新創(chuàng)建的線程執(zhí)行完成
new_thread.join().unwrap();
}
線程就永遠(yuǎn)都不會(huì)結(jié)束,如果你用的還是古董電腦,運(yùn)行上面的代碼之前,請(qǐng)做好心理準(zhǔn)備。在實(shí)際代碼中,要時(shí)刻警惕該情況的出現(xiàn)(單核情況下,CPU占用率會(huì)飆升到100%),除非你是故意為之。
線程結(jié)束的另一種方式就是,線程所在進(jìn)程結(jié)束了。我們把上面這個(gè)例子稍作修改:
use std::thread;
fn main() {
// 創(chuàng)建一個(gè)線程
thread::spawn(move || {
loop {
println!("I am a new thread.");
}
});
// 不等待新創(chuàng)建的線程執(zhí)行完成
// new_thread.join().unwrap();
}
同上面的代碼相比,唯一的差別在于main
函數(shù)的最后一行代碼被注釋了,這樣主線程就不用等待新建線程了,在創(chuàng)建線程之后就執(zhí)行完了,其所在進(jìn)程也就結(jié)束了,從而新建的線程也就結(jié)束了。此處,你可能有疑問:為什么一定是進(jìn)程結(jié)束導(dǎo)致新建線程結(jié)束?也可能是創(chuàng)建新線程的主線程結(jié)束而導(dǎo)致的?事實(shí)到底如何,我們不妨驗(yàn)證一下:
use std::thread;
fn main() {
// 創(chuàng)建一個(gè)線程
let new_thread = thread::spawn(move || {
// 再創(chuàng)建一個(gè)線程
thread::spawn(move || {
loop {
println!("I am a new thread.");
}
})
});
// 等待新創(chuàng)建的線程執(zhí)行完成
new_thread.join().unwrap();
println!("Child thread is finish!");
// 睡眠一段時(shí)間,看子線程創(chuàng)建的子線程是否還在運(yùn)行
thread::sleep_ms(100);
}
這次我們?cè)谛陆ň€程中還創(chuàng)建了一個(gè)線程,從而第一個(gè)新建線程是父線程,主線程在等待該父線程結(jié)束后,主動(dòng)睡眠一段時(shí)間。這樣做有兩個(gè)目的,一是確保整個(gè)程序不會(huì)馬上結(jié)束;二是如果子線程還存在,應(yīng)該會(huì)獲得執(zhí)行機(jī)會(huì),以此來(lái)檢驗(yàn)子線程是否還在運(yùn)行,下面是輸出結(jié)果:
Child thread is finish!
I am a new thread.
I am a new thread.
......
結(jié)果表明,在父線程結(jié)束后,其創(chuàng)建的子線程還活著,這并不會(huì)因?yàn)楦妇€程結(jié)束而結(jié)束。這個(gè)還是比較符合自然規(guī)律的,要不然真會(huì)斷子絕孫,人類滅絕。所以導(dǎo)致線程結(jié)束的第二種方式,是結(jié)束其所在進(jìn)程。到此為止,我們已經(jīng)把線程的創(chuàng)建和結(jié)束都介紹完了,那么接下來(lái)我們會(huì)介紹一些更有趣的東西。但是在此之前,請(qǐng)先考慮一下下面的練習(xí)題。
練習(xí)題:
有一組學(xué)生的成績(jī),我們需要對(duì)它們?cè)u(píng)分,90分及以上是A,80分及以上是B,70分及以上是C,60分及以上為D,60分以下為E。現(xiàn)在要求用Rust語(yǔ)言編寫一個(gè)程序來(lái)評(píng)分,且評(píng)分由新建的線程來(lái)做,最終輸出每個(gè)學(xué)生的學(xué)號(hào),成績(jī),評(píng)分。學(xué)生成績(jī)單隨機(jī)產(chǎn)生,學(xué)生人數(shù)100位,成績(jī)范圍為[0,100],學(xué)號(hào)依次從1開始,直到100。