Rust如何使用線程同時運行代碼
一、Rust 的線程模型
Rust 標準庫使用的是 1:1 的線程模型,即每一個語言層的線程都對應一個操作系統(tǒng)線程。Rust 中通過標準庫提供的 std::thread 模塊來創(chuàng)建、管理線程。
當然,也有一些第三方庫會采用不同的線程模型,或者利用異步(async)機制來實現(xiàn)并發(fā)(比如 Rust 的 async/await 機制),在面對具體需求時可以根據(jù)實際情況做選擇。
二、創(chuàng)建線程:thread::spawn
要在 Rust 中創(chuàng)建一個新線程,可以使用 thread::spawn 函數(shù),并向它傳遞一個閉包(closure)。閉包中包含需要在線程中執(zhí)行的代碼。
例如:
use std::thread;
use std::time::Duration;
fn main() {
// 使用 thread::spawn 創(chuàng)建新的線程
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
// 主線程也執(zhí)行一些操作
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}在上面的例子里,我們在子線程中打印數(shù)字,同時在主線程中也打印數(shù)字。由于操作系統(tǒng)會對線程進行調(diào)度,輸出的順序無法完全預測??赡苤骶€程先打印,也可能子線程先打印,或者兩者交錯執(zhí)行。
需要注意的是,當主線程結(jié)束時,所有通過 spawn 創(chuàng)建的子線程會被強制終止,即使子線程還沒有執(zhí)行完。
三、等待線程完成:JoinHandle 與 join
如果希望確保子線程的代碼一定會執(zhí)行完,那么就需要在主線程結(jié)束前等待子線程。thread::spawn 的返回值是一個 JoinHandle,可以用它來調(diào)用 join 方法,阻塞(block)當前線程,直到對應的子線程執(zhí)行完成。
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));
}
// 調(diào)用 join,阻塞主線程,直到子線程結(jié)束
handle.join().unwrap();
}當我們在主線程中調(diào)用 handle.join() 時,主線程會暫停執(zhí)行,直到子線程完成工作。這樣就能確保在程序退出前,所有線程都能順利完成執(zhí)行。
如果把 join 放在主線程的循環(huán)之前,那么主線程會先等待子線程結(jié)束,才會進行自身的打印操作——這樣就不會再看到主線程與子線程的輸出交錯了。
四、move 閉包與線程
多線程編程中常常需要在線程間傳遞數(shù)據(jù)或訪問主線程中的變量。
在 Rust 中,如果一個閉包想要捕獲外部變量,就要考慮該變量的所有權(quán)或引用生命周期問題。
4.1.問題場景
如下示例所示,如果我們在主線程中創(chuàng)建一個向量 v,然后在子線程中直接打印這個向量,就會出錯:
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {:?}", v);
});
// ...
handle.join().unwrap();
}編譯時,Rust 會提示閉包捕獲的是對 v 的引用,但無法保證在子線程運行時 v 依舊有效:主線程可能在子線程使用 v 之前就結(jié)束了,讓 v 不再有效,從而導致潛在的懸垂引用(dangling reference)。
4.2.使用 move 關(guān)鍵字
為了解決這個問題,需要在閉包前面加上 move 關(guān)鍵字,這樣可以把閉包中用到的外部數(shù)據(jù)移動到閉包的所有權(quán)中。
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();
}在這里,move 會把 v 的所有權(quán)從主線程轉(zhuǎn)移到子線程,從而保證子線程在使用 v 時不會遇到生命周期問題。不過需要注意的是,這樣一來,主線程就不能再使用 v 了,因為所有權(quán)已經(jīng)被移動出去了。
4.3.不能與 drop 共用所有權(quán)
如果嘗試同時在主線程中顯式調(diào)用 drop(v) 并且在子線程中使用 v,無論有沒有用 move,都行不通。Rust 的所有權(quán)規(guī)則會保證同一份數(shù)據(jù)不會被多次釋放或引用到失效的數(shù)據(jù)。所以,在設計多線程邏輯時,需要明確劃分數(shù)據(jù)的所有權(quán)與生命周期,以避免死鎖、競態(tài)條件或懸垂引用等問題。
五、小結(jié)
- Rust 標準庫中的線程模型:Rust 使用一對一(1:1)模型,每個語言線程對應一個系統(tǒng)線程。
- 創(chuàng)建線程:使用
thread::spawn來創(chuàng)建子線程,傳入一個閉包作為要執(zhí)行的代碼。 - 線程同步:通過返回的
JoinHandle調(diào)用join,可以阻塞主線程并等待子線程完成執(zhí)行。 - 所有權(quán)與生命周期:使用
move關(guān)鍵字將閉包所需的變量從主線程移動到子線程,從而避免引用沖突或無效引用。 - 小心共享數(shù)據(jù):當多個線程需要同時訪問或修改同一份數(shù)據(jù)時,需要使用安全的并發(fā)原語(例如
Mutex、RwLock、Arc等),否則會出現(xiàn)競態(tài)條件。
在 Rust 中編寫并發(fā)程序時,我們需要充分利用所有權(quán)與借用檢查器提供的安全保障,同時對多線程邏輯進行精心設計。盡管多線程編程能帶來性能上的提升,但也應關(guān)注潛在的風險,并通過 Rust 的工具鏈和語言特性來盡量減少錯誤,寫出更安全、更可靠的并發(fā)應用。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Rust使用Sled添加高性能嵌入式數(shù)據(jù)庫
這篇文章主要為大家詳細介紹了如何在Rust項目中使用Sled庫,一個為Rust生態(tài)設計的現(xiàn)代、高性能嵌入式數(shù)據(jù)庫,感興趣的小伙伴可以跟隨小編一起學習一下2024-03-03
解讀Rust的Rc<T>:實現(xiàn)多所有權(quán)的智能指針方式
Rc<T> 是 Rust 中用于多所有權(quán)的引用計數(shù)類型,通過增加引用計數(shù)來管理共享數(shù)據(jù),只有當最后一個引用離開作用域時,數(shù)據(jù)才會被釋放,Rc<T> 適用于單線程環(huán)境,并且只允許不可變共享數(shù)據(jù);需要可變共享時應考慮使用 RefCell<T> 或其他解決方案2025-02-02
Rust?HashMap詳解及單詞統(tǒng)計示例用法詳解
HashMap在Rust中是一個強大的工具,通過合理使用可以簡化很多與鍵值對相關(guān)的問題,在實際開發(fā)中,我們可以充分利用其特性,提高代碼的效率和可讀性,本文將深入介紹HashMap的特性,以及通過一個單詞統(tǒng)計的例子展示其用法,感興趣的朋友一起看看吧2024-02-02

