多執行緒
如果你使用多個執行緒 (Thread),你可以同時做很多事情。現代電腦有一個以上的核心 (Core),所以它們可以同時做多件事情,Rust 讓你能運用它們。Rust 使用的執行緒被稱為"OS 執行緒"。OS 執行緒的意思是作業系統在不同的核心上建立執行緒。(其他一些語言使用功能沒那麼強大的"green threads")
你要用 std::thread::spawn
建立執行緒,以及用閉包來告訴它該怎麼做。執行緒很有趣,因為它們同時執行,你可以測試它看看會發生什麼。這裡是個簡單的範例:
fn main() { std::thread::spawn(|| { println!("I am printing something"); }); }
如果你執行它,每次結果都會不同。有時會印出來,有時不會(這也取決於你的電腦速度)。這是因為有時 main()
比執行緒還早結束。而當 main()
完成後,程式就終結了。這在 for
迴圈中更容易觀察到:
fn main() { for _ in 0..10 { // 設置十個執行緒 std::thread::spawn(|| { println!("I am printing something"); }); } // 現在執行緒啟動了. } // 有多少能在這裡的 main() 結束之前完成?
在 main
結束之前,通常大約會有四條執行緒印出來,但總是不一樣。如果你的電腦速度比較快,那麼可能就印不出來了。另外,有時執行緒會恐慌:
thread 'thread 'I am printing something
thread '<unnamed><unnamed>thread '' panicked at '<unnamed>I am printing something
' panicked at 'thread '<unnamed>cannot access stdout during shutdown' panicked at '<unnamed>thread 'cannot access stdout during
shutdown
這是程式正在關閉時,執行緒試圖做一些事情時會出現的錯誤。
你可以給電腦做些事,這樣它就不會馬上關閉了:
fn main() { for _ in 0..10 { std::thread::spawn(|| { println!("I am printing something"); }); } for _ in 0..1_000_000 { // 讓電腦宣告 "let x = 9" 一百萬次 // 它要在它可以離開 main 函式前完成這件事 let _x = 9; } }
但這是個讓執行緒有時間完成的蠢方法。更好的方式是將執行緒繫結到變數上。如果你加上 let
,你就能建立 JoinHandle
。你可以在 spawn
的簽名中看到這一點:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
f
是閉包──我們將在後面學到如何將閉包放入我們的函式中
所以現在我們每次都有 JoinHandle
。
fn main() { for _ in 0..10 { let handle = std::thread::spawn(|| { println!("I am printing something"); }); } }
handle
現在是個 JoinHandle
。我們怎麼處理它呢?我們要使用叫做 .join()
的方法。這個方法的意思是"等待所有執行緒完成"(它等待執行緒加入它)。所以現在只要寫 handle.join()
,它就會等待每個執行緒完成。
fn main() { for _ in 0..10 { let handle = std::thread::spawn(|| { println!("I am printing something"); }); handle.join(); // 等待執行緒完成 } }
現在我們就來了解一下閉包的三種類型。這三種類型是
FnOnce
:接受整個值FnMut
:接受可變參考Fn
:接受常規參考
如果可以閉包會盡量試著使用 Fn
。但如果它需要改變值,它將使用 FnMut
,而如果它需要接受整個值,它將使用 FnOnce
。FnOnce
是個好名字,因為這解釋了它做了什麼:它接受一次值,然後就不能再拿了。
這裡是範例:
fn main() { let my_string = String::from("I will go into the closure"); let my_closure = || println!("{}", my_string); my_closure(); my_closure(); }
String
不能 Copy
,所以 my_closure()
是個 Fn
:它拿到參考。
如果我們改變 my_string
,它會變成 FnMut
。
fn main() { let mut my_string = String::from("I will go into the closure"); let mut my_closure = || { my_string.push_str(" now"); println!("{}", my_string); }; my_closure(); my_closure(); }
印出:
I will go into the closure now
I will go into the closure now now
而如果拿值來用,則會是 FnOnce
。
fn main() { let my_vec: Vec<i32> = vec![8, 9, 10]; let my_closure = || { my_vec .into_iter() // into_iter takes ownership .map(|x| x as u8) // turn it into u8 .map(|x| x * 2) // multiply by 2 .collect::<Vec<u8>>() // collect into a Vec }; let new_vec = my_closure(); println!("{:?}", new_vec); }
我們拿值來用,所以我們無法再執行一次 my_closure()
。就是這個名字的由來。
那麼現在回到執行緒。讓我們試著使用外面的值:
fn main() { let mut my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(|| { println!("{}", my_string); // ⚠️ }); handle.join(); }
編譯器說這樣不行。
error[E0373]: closure may outlive the current function, but it borrows `my_string`, which is owned by the current function
--> src\main.rs:28:37
|
28 | let handle = std::thread::spawn(|| {
| ^^ may outlive borrowed value `my_string`
29 | println!("{}", my_string);
| --------- `my_string` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src\main.rs:28:18
|
28 | let handle = std::thread::spawn(|| {
| __________________^
29 | | println!("{}", my_string);
30 | | });
| |______^
help: to force the closure to take ownership of `my_string` (and any other referenced variables), use the `move` keyword
|
28 | let handle = std::thread::spawn(move || {
| ^^^^^^^
這條訊息很長,但很有用:它說到 use the `move` keyword
。問題是我們雖然可以在執行緒裡使用 my_string
時對它做任何事情,但執行緒卻不擁有它。因為那樣會不安全。
讓我們試試其他行不通的方式:
fn main() { let mut my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(|| { println!("{}", my_string); // 現在 my_string 被拿來當參考使用 }); std::mem::drop(my_string); // ⚠️ 我們嘗試在這丟棄它. 但執行緒仍然需要它. handle.join(); }
所以你要用 move
來拿走值。現在安全了:
fn main() { let mut my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(move|| { println!("{}", my_string); }); std::mem::drop(my_string); // ⚠️ 我們無法丟棄, 因為 handle 擁有它. 因此這將會無法執行 handle.join(); }
所以當我們把 std::mem::drop
刪掉,現在就可以用了。在 handle
拿走 my_string
後,我們的程式碼就安全了。
fn main() { let my_string = String::from("Can I go inside the thread?"); let handle = std::thread::spawn(move|| { println!("{}", my_string); }); handle.join().unwrap(); }
所以只要記住:如果你需要從外面取得某個執行緒裡面的值,你需要使用 move
。