ch16-01-threads.md
commit 6b9eae8ce91dd0d94982795762d22077d372e90c
在大部分現(xiàn)代操作系統(tǒng)中,已執(zhí)行程序的代碼在一個(gè) 進(jìn)程(process)中運(yùn)行,操作系統(tǒng)則負(fù)責(zé)管理多個(gè)進(jìn)程。在程序內(nèi)部,也可以擁有多個(gè)同時(shí)運(yùn)行的獨(dú)立部分。運(yùn)行這些獨(dú)立部分的功能被稱為 線程(threads)。
將程序中的計(jì)算拆分進(jìn)多個(gè)線程可以改善性能,因?yàn)槌绦蚩梢酝瑫r(shí)進(jìn)行多個(gè)任務(wù),不過(guò)這也會(huì)增加復(fù)雜性。因?yàn)榫€程是同時(shí)運(yùn)行的,所以無(wú)法預(yù)先保證不同線程中的代碼的執(zhí)行順序。這會(huì)導(dǎo)致諸如此類的問(wèn)題:
Rust 嘗試減輕使用線程的負(fù)面影響。不過(guò)在多線程上下文中編程仍需格外小心,同時(shí)其所要求的代碼結(jié)構(gòu)也不同于運(yùn)行于單線程的程序。
編程語(yǔ)言有一些不同的方法來(lái)實(shí)現(xiàn)線程。很多操作系統(tǒng)提供了創(chuàng)建新線程的 API。這種由編程語(yǔ)言調(diào)用操作系統(tǒng) API 創(chuàng)建線程的模型有時(shí)被稱為 1:1,一個(gè) OS 線程對(duì)應(yīng)一個(gè)語(yǔ)言線程。Rust 標(biāo)準(zhǔn)庫(kù)只提供了 1:1 線程實(shí)現(xiàn);有一些 crate 實(shí)現(xiàn)了其他有著不同取舍的線程模型。
為了創(chuàng)建一個(gè)新線程,需要調(diào)用 thread::spawn
函數(shù)并傳遞一個(gè)閉包(第十三章學(xué)習(xí)了閉包),并在其中包含希望在新線程運(yùn)行的代碼。示例 16-1 中的例子在主線程打印了一些文本而另一些文本則由新線程打?。?br>
文件名: src/main.rs
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
示例 16-1: 創(chuàng)建一個(gè)打印某些內(nèi)容的新線程,但是主線程打印其它內(nèi)容
注意這個(gè)函數(shù)編寫的方式,當(dāng)主線程結(jié)束時(shí),新線程也會(huì)結(jié)束,而不管其是否執(zhí)行完畢。這個(gè)程序的輸出可能每次都略有不同,不過(guò)它大體上看起來(lái)像這樣:
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
thread::sleep
調(diào)用強(qiáng)制線程停止執(zhí)行一小段時(shí)間,這會(huì)允許其他不同的線程運(yùn)行。這些線程可能會(huì)輪流運(yùn)行,不過(guò)并不保證如此:這依賴操作系統(tǒng)如何調(diào)度線程。在這里,主線程首先打印,即便新創(chuàng)建線程的打印語(yǔ)句位于程序的開(kāi)頭,甚至即便我們告訴新建的線程打印直到 i
等于 9 ,它在主線程結(jié)束之前也只打印到了 5。
如果運(yùn)行代碼只看到了主線程的輸出,或沒(méi)有出現(xiàn)重疊打印的現(xiàn)象,嘗試增大區(qū)間 (變量 i
的范圍) 來(lái)增加操作系統(tǒng)切換線程的機(jī)會(huì)。
由于主線程結(jié)束,示例 16-1 中的代碼大部分時(shí)候不光會(huì)提早結(jié)束新建線程,甚至不能實(shí)際保證新建線程會(huì)被執(zhí)行。其原因在于無(wú)法保證線程運(yùn)行的順序!
可以通過(guò)將 thread::spawn
的返回值儲(chǔ)存在變量中來(lái)修復(fù)新建線程部分沒(méi)有執(zhí)行或者完全沒(méi)有執(zhí)行的問(wèn)題。thread::spawn
的返回值類型是 JoinHandle
。JoinHandle
是一個(gè)擁有所有權(quán)的值,當(dāng)對(duì)其調(diào)用 join
方法時(shí),它會(huì)等待其線程結(jié)束。示例 16-2 展示了如何使用示例 16-1 中創(chuàng)建的線程的 JoinHandle
并調(diào)用 join
來(lái)確保新建線程在 main
退出前結(jié)束運(yùn)行:
文件名: src/main.rs
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
示例 16-2: 從 thread::spawn
保存一個(gè) JoinHandle
以確保該線程能夠運(yùn)行至結(jié)束
通過(guò)調(diào)用 handle 的 join
會(huì)阻塞當(dāng)前線程直到 handle 所代表的線程結(jié)束。阻塞(Blocking) 線程意味著阻止該線程執(zhí)行工作或退出。因?yàn)槲覀儗?nbsp;join
調(diào)用放在了主線程的 for
循環(huán)之后,運(yùn)行示例 16-2 應(yīng)該會(huì)產(chǎn)生類似這樣的輸出:
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
這兩個(gè)線程仍然會(huì)交替執(zhí)行,不過(guò)主線程會(huì)由于 handle.join()
調(diào)用會(huì)等待直到新建線程執(zhí)行完畢。
不過(guò)讓我們看看將 handle.join()
移動(dòng)到 main
中 for
循環(huán)之前會(huì)發(fā)生什么,如下:
文件名: src/main.rs
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
主線程會(huì)等待直到新建線程執(zhí)行完畢之后才開(kāi)始執(zhí)行 for
循環(huán),所以輸出將不會(huì)交替出現(xiàn),如下所示:
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
諸如將 join
放置于何處這樣的小細(xì)節(jié),會(huì)影響線程是否同時(shí)運(yùn)行。
move
關(guān)鍵字經(jīng)常用于傳遞給 thread::spawn
的閉包,因?yàn)殚]包會(huì)獲取從環(huán)境中取得的值的所有權(quán),因此會(huì)將這些值的所有權(quán)從一個(gè)線程傳送到另一個(gè)線程。在第十三章 “閉包會(huì)捕獲其環(huán)境” 部分討論了閉包上下文中的 move
?,F(xiàn)在我們會(huì)更專注于 move
和 thread::spawn
之間的交互。
在第十三章中,我們講到可以在參數(shù)列表前使用 move
關(guān)鍵字強(qiáng)制閉包獲取其使用的環(huán)境值的所有權(quán)。這個(gè)技巧在創(chuàng)建新線程將值的所有權(quán)從一個(gè)線程移動(dòng)到另一個(gè)線程時(shí)最為實(shí)用。
注意示例 16-1 中傳遞給 thread::spawn
的閉包并沒(méi)有任何參數(shù):并沒(méi)有在新建線程代碼中使用任何主線程的數(shù)據(jù)。為了在新建線程中使用來(lái)自于主線程的數(shù)據(jù),需要新建線程的閉包獲取它需要的值。示例 16-3 展示了一個(gè)嘗試在主線程中創(chuàng)建一個(gè) vector 并用于新建線程的例子,不過(guò)這么寫還不能工作,如下所示:
文件名: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
示例 16-3: 嘗試在另一個(gè)線程使用主線程創(chuàng)建的 vector
閉包使用了 v
,所以閉包會(huì)捕獲 v
并使其成為閉包環(huán)境的一部分。因?yàn)?nbsp;thread::spawn
在一個(gè)新線程中運(yùn)行這個(gè)閉包,所以可以在新線程中訪問(wèn) v
。然而當(dāng)編譯這個(gè)例子時(shí),會(huì)得到如下錯(cuò)誤:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
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
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {:?}", v);
8 | | });
| |______^
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 || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` due to previous error
Rust 會(huì) 推斷 如何捕獲 v
,因?yàn)?nbsp;println!
只需要 v
的引用,閉包嘗試借用 v
。然而這有一個(gè)問(wèn)題:Rust 不知道這個(gè)新建線程會(huì)執(zhí)行多久,所以無(wú)法知曉 v
的引用是否一直有效。
示例 16-4 展示了一個(gè) v
的引用很有可能不再有效的場(chǎng)景:
文件名: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
drop(v); // oh no!
handle.join().unwrap();
}
示例 16-4: 一個(gè)具有閉包的線程,嘗試使用一個(gè)在主線程中被回收的引用 v
假如這段代碼能正常運(yùn)行的話,則新建線程則可能會(huì)立刻被轉(zhuǎn)移到后臺(tái)并完全沒(méi)有機(jī)會(huì)運(yùn)行。新建線程內(nèi)部有一個(gè) v
的引用,不過(guò)主線程立刻就使用第十五章討論的 drop
丟棄了 v
。接著當(dāng)新建線程開(kāi)始執(zhí)行,v
已不再有效,所以其引用也是無(wú)效的。噢,這太糟了!
為了修復(fù)示例 16-3 的編譯錯(cuò)誤,我們可以聽(tīng)取錯(cuò)誤信息的建議:
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 || {
| ++++
通過(guò)在閉包之前增加 move
關(guān)鍵字,我們強(qiáng)制閉包獲取其使用的值的所有權(quán),而不是任由 Rust 推斷它應(yīng)該借用值。示例 16-5 中展示的對(duì)示例 16-3 代碼的修改,可以按照我們的預(yù)期編譯并運(yùn)行:
文件名: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
示例 16-5: 使用 move
關(guān)鍵字強(qiáng)制獲取它使用的值的所有權(quán)
那么如果使用了 move
閉包,示例 16-4 中主線程調(diào)用了 drop
的代碼會(huì)發(fā)生什么呢?加了 move
就搞定了嗎?不幸的是,我們會(huì)得到一個(gè)不同的錯(cuò)誤,因?yàn)槭纠?16-4 所嘗試的操作由于一個(gè)不同的原因而不被允許。如果為閉包增加 move
,將會(huì)把 v
移動(dòng)進(jìn)閉包的環(huán)境中,如此將不能在主線程中對(duì)其調(diào)用 drop
了。我們會(huì)得到如下不同的編譯錯(cuò)誤:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {:?}", v);
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` due to previous error
Rust 的所有權(quán)規(guī)則又一次幫助了我們!示例 16-3 中的錯(cuò)誤是因?yàn)?Rust 是保守的并只會(huì)為線程借用 v
,這意味著主線程理論上可能使新建線程的引用無(wú)效。通過(guò)告訴 Rust 將 v
的所有權(quán)移動(dòng)到新建線程,我們向 Rust 保證主線程不會(huì)再使用 v
。如果對(duì)示例 16-4 也做出如此修改,那么當(dāng)在主線程中使用 v
時(shí)就會(huì)違反所有權(quán)規(guī)則。 move
關(guān)鍵字覆蓋了 Rust 默認(rèn)保守的借用,但它不允許我們違反所有權(quán)規(guī)則。
現(xiàn)在我們對(duì)線程和線程 API 有了基本的了解,讓我們討論一下使用線程實(shí)際可以 做 什么吧。
更多建議: