淺析Rust多線程中如何安全的使用變量
在Rust語言中,一個(gè)既引人入勝又可能帶來挑戰(zhàn)的特性是閉包如何從其所在環(huán)境中捕獲變量,尤其是在涉及多線程編程的情境下。
如果嘗試在不使用move
關(guān)鍵字的情況下創(chuàng)建新線程并傳遞數(shù)據(jù)至閉包內(nèi),編譯器將很可能返回一系列與生命周期、借用規(guī)則及所有權(quán)相關(guān)的復(fù)雜錯(cuò)誤信息。
不過,這種機(jī)制雖然增加了學(xué)習(xí)曲線,但也確保了內(nèi)存安全與并發(fā)執(zhí)行中的數(shù)據(jù)一致性。
本文我們將探討如何在線程的閉包中安全的使用變量,包括共享變量和修改變量。
1. 向線程傳遞變量
首先,我們構(gòu)造一個(gè)簡單的示例,在線程中正常使用一個(gè)外部的變量,看看Rust中能否正常編譯運(yùn)行。
use std::thread; fn main() { let msg = String::from("Hello World!"); let handle = thread::spawn(|| { // msg 是主線中定義的變量 println!("{}", msg); }); handle.join().unwrap(); }
例子非常簡單,看著寫法也沒什么問題,在其他編程語言中類似的寫法是沒有問題的。
但是,使用cargo run
運(yùn)行時(shí),卻有如下的錯(cuò)誤:
為什么會(huì)有這樣的錯(cuò)誤?這就是Rust
在內(nèi)存方面更加嚴(yán)謹(jǐn)?shù)脑颉?/p>
上面Rust
的錯(cuò)誤信息中也給出了原因,總結(jié)起來主要有兩點(diǎn):
- 線程的生命周期:新創(chuàng)建的線程的生命周期有可能超出主函數(shù)
main
的執(zhí)行范圍。當(dāng)main
函數(shù)終止時(shí),與之相關(guān)的局部變量(也就是msg
)將超出作用域。 - 不符合借用規(guī)則:在
Rust
中,引用的生命周期不會(huì)超過其所指向數(shù)據(jù)的生命周期,以避免出現(xiàn)懸空引用。如果main提前結(jié)束,那么線程中的msg將成為懸空引用。
修復(fù)的方法很簡單,使用move
關(guān)鍵字,將變量的所有權(quán)轉(zhuǎn)移到線程中就可以了。
let handle = thread::spawn(move || { // msg 是主線中定義的變量 println!("{}", msg); });
這樣就可以正常運(yùn)行了。
不過,這樣,主線程中就無法使用變量msg
了,比如在main
函數(shù)的最后打印msg
,會(huì)報(bào)錯(cuò),因?yàn)樗乃袡?quán)已經(jīng)轉(zhuǎn)移到線程中了。
2. 多線程共享變量引用
如果我們只把變量的引用轉(zhuǎn)移給線程,是不是可以在主線程main
中繼續(xù)使用變量msg
呢?
use std::thread; fn main() { let msg = String::from("Hello World!"); let msg_ref = &msg; let handle = { thread::spawn(move || { // msg 是主線中定義的變量 println!("{}", msg_ref); }) }; handle.join().unwrap(); println!("msg in main : {}", msg_ref); }
很遺憾,依然有錯(cuò)誤:
錯(cuò)誤的原因仍然是傳入線程中的變量引用msg_ref
生命周期的不夠長。
雖然我們使用了move
,將msg_ref
轉(zhuǎn)移到線程中,但main
中仍然擁有底層的數(shù)據(jù)msg
,
一旦main
函數(shù)結(jié)束(或者數(shù)據(jù)在線程完成之前超出范圍),該引用(msg_ref)指向數(shù)據(jù)將失去有效的內(nèi)存,成為懸空引用。
總的來說就是:
- 移動(dòng)引用并不移動(dòng)原始數(shù)據(jù)-只轉(zhuǎn)移引用本身的所有權(quán)
- 實(shí)際數(shù)據(jù)(
msg
)仍然由原始范圍擁有,并具有自己的生命周期約束
為了修復(fù)這個(gè)錯(cuò)誤,就要用到Rust
中提供的并發(fā)原語Arc
(一種自動(dòng)引用計(jì)數(shù)的智能指針)。
先看看使用Arc
修改后的例子。
use std::sync::Arc; use std::thread; fn main() { let msg = String::from("Hello World!"); // 通過Arc來創(chuàng)建變量的引用 let msg_ref = Arc::new(msg); // 線程1 let handle_1 = { // move 之前,先使用Arc clone 變量 let msg_thread = Arc::clone(&msg_ref); thread::spawn(move || { println!("Thread 1: {}", msg_thread); }) }; // 線程2 let handle_2 = { let msg_thread = Arc::clone(&msg_ref); thread::spawn(move || { println!("Thread 2: {}", msg_thread); }) }; handle_1.join().unwrap(); handle_2.join().unwrap(); // 主線程中依然可以使用變量 println!("msg in main : {}", msg_ref); }
使用Arc
修改之后,變量不僅可以在多個(gè)線程中共享,主線程中也可以使用。
3. 多線程中修改變量
上面的示例是在多個(gè)線程中共享變量,如果想要修改變量的話,那么就會(huì)出現(xiàn)數(shù)據(jù)競爭的情況。
這時(shí),就要用到Rust
的另一個(gè)并發(fā)原語Mutex
。
use std::sync::{Arc, Mutex}; use std::thread; fn main() { // 創(chuàng)建一個(gè)被Mutex保護(hù)的共享數(shù)據(jù),這里是一個(gè)i32類型的數(shù)字 let shared_number = Arc::new(Mutex::new(0)); // 定義一個(gè)線程向量,用于存儲(chǔ)創(chuàng)建的線程 let mut threads = Vec::new(); // 創(chuàng)建10個(gè)線程,每個(gè)線程對(duì)共享數(shù)據(jù)進(jìn)行1000次遞增操作 for _ in 0..10 { // 克隆Arc,使得每個(gè)線程都擁有一個(gè)指向共享數(shù)據(jù)的引用 let num_clone = Arc::clone(&shared_number); let handle = thread::spawn(move || { // 嘗試獲取Mutex的鎖,這是一個(gè)阻塞操作,如果鎖不可用,線程會(huì)等待 let mut num = num_clone.lock().unwrap(); for _ in 0..1000 { *num += 1; } }); threads.push(handle); } // 等待所有線程完成操作 for handle in threads { handle.join().unwrap(); } // 獲取最終的共享數(shù)據(jù)值并打印 let final_num = shared_number.lock().unwrap(); println!("最終10個(gè)線程的累加結(jié)果: {}", final_num); }
在這個(gè)示例中:
- 首先創(chuàng)建了一個(gè)
Arc<Mutex<i32>>
類型的共享數(shù)據(jù),Arc
用于在多個(gè)線程間共享Mutex
,Mutex
用于保護(hù)內(nèi)部的i32
數(shù)據(jù)。 - 循環(huán)創(chuàng)建
10
個(gè)線程,每個(gè)線程都克隆了Arc
并嘗試獲取Mutex
的鎖。一旦獲取到鎖,線程就可以安全地對(duì)共享數(shù)據(jù)進(jìn)行遞增操作。 - 主線程使用
join
方法等待所有子線程完成操作。 - 最后,主線程獲取并打印共享數(shù)據(jù)的最終值。由于Mutex的保護(hù),多個(gè)線程對(duì)共享數(shù)據(jù)的操作不會(huì)產(chǎn)生數(shù)據(jù)競爭,保證了數(shù)據(jù)的一致性。
運(yùn)行結(jié)果:
10
個(gè)線程,每個(gè)累加1000
,所以最后結(jié)果是1000*10=10000
。
4. 總結(jié)
從上面的例子可以看出,Rust
的閉包捕獲規(guī)則最初可能感覺很嚴(yán)格,但它們?cè)诖_保內(nèi)存安全和數(shù)據(jù)競爭自由方面至關(guān)重要。
總之,
如果需要在另一個(gè)線程中擁有數(shù)據(jù),考慮使用move
;
如果需要跨線程共享數(shù)據(jù),考慮使用Arc
;
如果需要跨線程共享和修改數(shù)據(jù),考慮使用Arc+Mutex
;
到此這篇關(guān)于淺析Rust多線程中如何安全的使用變量的文章就介紹到這了,更多相關(guān)Rust多線程使用變量內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用VSCode配置Rust開發(fā)環(huán)境(Rust新手教程)
這篇文章主要介紹了如何使用VSCode配置Rust開發(fā)環(huán)境(Rust新手教程),本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07Rust-使用dotenvy加載和使用環(huán)境變量的過程詳解
系統(tǒng)的開發(fā),測試和部署離不開環(huán)境變量,今天分享在Rust的系統(tǒng)開發(fā)中,使用dotenvy來讀取和使用環(huán)境變量,感興趣的朋友跟隨小編一起看看吧2023-11-11