深圳做網(wǎng)站知名排行發(fā)廣告平臺(tái)有哪些免費(fèi)
安全且高效地處理并發(fā)編程是 Rust 的另一個(gè)主要目標(biāo)。并發(fā)編程(Concurrent programming),代表程序的不同部分相互獨(dú)立地執(zhí)行,而并行編程(parallel programming)代表程序不同部分同時(shí)執(zhí)行,這兩個(gè)概念隨著計(jì)算機(jī)越來(lái)越多的利用多處理器的優(yōu)勢(shì)而顯得愈發(fā)重要。由于歷史原因,在此類上下文中編程一直是困難且容易出錯(cuò)的:Rust 希望能改變這一點(diǎn)。
起初,Rust 團(tuán)隊(duì)認(rèn)為確保內(nèi)存安全和防止并發(fā)問(wèn)題是兩個(gè)分別需要不同方法應(yīng)對(duì)的挑戰(zhàn)。隨著時(shí)間的推移,團(tuán)隊(duì)發(fā)現(xiàn)所有權(quán)和類型系統(tǒng)是一系列解決內(nèi)存安全和并發(fā)問(wèn)題的強(qiáng)有力的工具!通過(guò)利用所有權(quán)和類型檢查,在 Rust 中很多并發(fā)錯(cuò)誤都是編譯時(shí)錯(cuò)誤,而非運(yùn)行時(shí)錯(cuò)誤。因此,相比花費(fèi)大量時(shí)間嘗試重現(xiàn)運(yùn)行時(shí)并發(fā) bug 出現(xiàn)的特定情況,Rust 會(huì)拒絕編譯不正確的代碼并提供解釋問(wèn)題的錯(cuò)誤信息。因此,你可以在開(kāi)發(fā)時(shí)修復(fù)代碼,而不是在部署到生產(chǎn)環(huán)境后修復(fù)代碼。
一、線程
在大部分現(xiàn)代操作系統(tǒng)中,已執(zhí)行程序的代碼在一個(gè)進(jìn)程(process)中運(yùn)行,操作系統(tǒng)則會(huì)負(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)題:
- 競(jìng)態(tài)條件(Race conditions),多個(gè)線程以不一致的順序訪問(wèn)數(shù)據(jù)或資源
- 死鎖(Deadlocks),兩個(gè)線程相互等待對(duì)方,這會(huì)阻止兩者繼續(xù)運(yùn)行
- 只會(huì)發(fā)生在特定情況且難以穩(wěn)定重現(xiàn)和修復(fù)的 bug
Rust 嘗試減輕使用線程的負(fù)面影響。不過(guò)在多線程上下文中編程仍需格外小心,同時(shí)其所要求的代碼結(jié)構(gòu)也不同于運(yùn)行于單線程的程序。
編程語(yǔ)言有一些不同的方法來(lái)實(shí)現(xiàn)線程,而且很多操作系統(tǒng)提供了創(chuàng)建新線程的 API。Rust 標(biāo)準(zhǔn)庫(kù)使用 1:1 線程實(shí)現(xiàn),這代表程序的每一個(gè)語(yǔ)言級(jí)線程使用一個(gè)系統(tǒng)線程。
在 Rust 中,你可以使用 std::thread::spawn
來(lái)創(chuàng)建多個(gè)線程。以下是一個(gè)創(chuàng)建多個(gè)線程并讓它們執(zhí)行不同任務(wù)的例子:
use std::thread;
use std::time::Duration;fn main() {// 創(chuàng)建一個(gè)向量來(lái)存儲(chǔ)JoinHandlelet mut handles: Vec<thread::JoinHandle<()>> = Vec::new();// 創(chuàng)建并啟動(dòng)多個(gè)線程for i in 0..5 {let handle = thread::spawn(move || {// 打印線程編號(hào)println!("Thread number {} is running", i);// 模擬一些工作thread::sleep(Duration::from_millis(500 * i));// 打印線程完成的消息println!("Thread number {} finished", i);});// 將JoinHandle存儲(chǔ)在向量中handles.push(handle);}// 等待所有線程結(jié)束for handle in handles {handle.join().unwrap();}println!("All threads have finished execution.");
}
在這個(gè)例子中,我們首先創(chuàng)建了一個(gè) Vec<thread::JoinHandle<()>>
來(lái)存儲(chǔ)每個(gè)線程的 JoinHandle
。然后,我們使用一個(gè) for
循環(huán)來(lái)創(chuàng)建 5 個(gè)線程。每個(gè)線程都執(zhí)行一個(gè)移動(dòng)(move
)閉包,該閉包打印線程編號(hào),休眠一段時(shí)間,然后打印完成消息。移動(dòng)閉包通過(guò) move
關(guān)鍵字捕獲循環(huán)變量 i
的值,使得每個(gè)線程都有自己的 i
副本。
在所有線程創(chuàng)建之后,我們遍歷 handles
向量,調(diào)用每個(gè) JoinHandle
的 join
方法。這會(huì)阻塞主線程直到對(duì)應(yīng)的線程結(jié)束。如果線程成功結(jié)束,join
方法會(huì)返回 ()
。如果線程由于 panic 而結(jié)束,join
方法會(huì)返回一個(gè)錯(cuò)誤,這里我們使用 unwrap
來(lái)獲取結(jié)果并忽略潛在的錯(cuò)誤。最后,當(dāng)所有線程都完成后,我們?cè)谥骶€程中打印一條消息。
請(qǐng)注意,在實(shí)際應(yīng)用中,你可能需要更細(xì)致地處理 join
方法返回的結(jié)果,以確保能夠適當(dāng)?shù)仨憫?yīng)線程中的 panic。
二、消息傳遞
一個(gè)日益流行的確保安全并發(fā)的方式是消息傳遞(message passing),這里線程或 actor 通過(guò)發(fā)送包含數(shù)據(jù)的消息來(lái)相互溝通。這個(gè)思想來(lái)源于 Go 編程語(yǔ)言文檔中 的口號(hào):“不要通過(guò)共享內(nèi)存來(lái)通訊;而是通過(guò)通訊來(lái)共享內(nèi)存”。
為了實(shí)現(xiàn)消息傳遞并發(fā),Rust 標(biāo)準(zhǔn)庫(kù)提供了一個(gè)信道(channel)實(shí)現(xiàn)。信道是一個(gè)通用編程概念,表示數(shù)據(jù)從一個(gè)線程發(fā)送到另一個(gè)線程。編程中的信息渠道(信道)有兩部分組成,一個(gè)發(fā)送者(transmitter)和一個(gè)接收者(receiver)。代碼中的一部分調(diào)用發(fā)送者的方法以及希望發(fā)送的數(shù)據(jù),另一部分則檢查接收端收到的消息。當(dāng)發(fā)送者或接收者任一被丟棄時(shí)可以認(rèn)為信道被關(guān)閉(closed)了。
mpsc::channel
函數(shù)創(chuàng)建一個(gè)新的信道;mpsc
是多個(gè)生產(chǎn)者,單個(gè)消費(fèi)者(multiple producer, single consumer)的縮寫(xiě)。簡(jiǎn)而言之,Rust 標(biāo)準(zhǔn)庫(kù)實(shí)現(xiàn)信道的方式意味著一個(gè)信道可以有多個(gè)產(chǎn)生值的發(fā)送(sending)端,但只能有一個(gè)消費(fèi)這些值的接收(receiving)端。
mpsc::channel
函數(shù)返回一個(gè)元組:第一個(gè)元素是發(fā)送端,而第二個(gè)元素是接收端。由于歷史原因,tx
和 rx
通常作為發(fā)送者(transmitter)和接收者(receiver)的縮寫(xiě)。
信道的發(fā)送端有一個(gè) send
方法用來(lái)獲取需要放入信道的值。send
方法返回一個(gè) Result<T, E>
類型,所以如果接收端已經(jīng)被丟棄了,將沒(méi)有發(fā)送值的目標(biāo),所以發(fā)送操作會(huì)返回錯(cuò)誤。
信道的接收者有兩個(gè)有用的方法:recv
和 try_recv
。 recv
是 receive
的縮寫(xiě)。這個(gè)方法會(huì)阻塞線程執(zhí)行直到從信道中接收一個(gè)值。一旦發(fā)送了一個(gè)值,recv
會(huì)在一個(gè) Result<T, E>
中返回它。當(dāng)信道發(fā)送端關(guān)閉,recv
會(huì)返回一個(gè)錯(cuò)誤表明不會(huì)再有新的值到來(lái)了。
try_recv
不會(huì)阻塞,相反它立刻返回一個(gè) Result<T, E>
:Ok
值包含可用的信息,而 Err
值代表此時(shí)沒(méi)有任何消息。如果線程在等待消息過(guò)程中還有其他工作時(shí)使用 try_recv
很有用:可以編寫(xiě)一個(gè)循環(huán)來(lái)頻繁調(diào)用 try_recv
,在有可用消息時(shí)進(jìn)行處理,其余時(shí)候則處理一會(huì)其他工作直到再次檢查。
2.1 信道與所有權(quán)轉(zhuǎn)移
這里嘗試在通過(guò) tx.send
發(fā)送 data
到信道中之后將其打印出來(lái)。允許這么做是一個(gè)壞主意:一旦將值發(fā)送到另一個(gè)線程后,那個(gè)線程可能會(huì)在我們?cè)俅问褂盟熬蛯⑵湫薷幕蛘邅G棄。其他線程對(duì)值可能的修改會(huì)由于不一致或不存在的數(shù)據(jù)而導(dǎo)致錯(cuò)誤或意外的結(jié)果。
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();let sender_thread = thread::spawn(move || {let data = "Hello from sender!".to_string();tx.send(data).unwrap();println!("Sended: {}", data);});let receiver_thread = thread::spawn(move || {let received = rx.recv().unwrap();println!("Received: {}", received);});sender_thread.join().unwrap();receiver_thread.join().unwrap();
}
編譯這一段代碼會(huì)報(bào)錯(cuò):
Compiling playground v0.0.1 (/playground)
error[E0382]: borrow of moved value: `data`--> src/main.rs:10:32|
8 | let data = "Hello from sender!".to_string();| ---- move occurs because `data` has type `String`, which does not implement the `Copy` trait
9 | tx.send(data).unwrap();| ---- value moved here
10 | println!("Sended: {}", data);| ^^^^ value borrowed here after move|= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
我們的并發(fā)錯(cuò)誤會(huì)造成一個(gè)編譯時(shí)錯(cuò)誤。send
函數(shù)獲取其參數(shù)的所有權(quán)并移動(dòng)這個(gè)值歸接收者所有。這可以防止在發(fā)送后再次意外地使用這個(gè)值;所有權(quán)系統(tǒng)檢查一切是否合乎規(guī)則。
修正這段代碼,就是把 tx.send
之后的打印語(yǔ)句去除,程序就可以正常運(yùn)行了。
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();let sender_thread = thread::spawn(move || {let data = "Hello from sender!".to_string();tx.send(data).unwrap();});let receiver_thread = thread::spawn(move || {let received = rx.recv().unwrap();println!("Received: {}", received);});sender_thread.join().unwrap();receiver_thread.join().unwrap();
}
運(yùn)行結(jié)果
Received: Hello from sender!
在這個(gè)示例中,我們創(chuàng)建了一個(gè)信道 (tx, rx)
,tx
是發(fā)送端,rx
是接收端。我們創(chuàng)建了一個(gè)線程 sender_thread
來(lái)發(fā)送數(shù)據(jù),并將數(shù)據(jù)的所有權(quán)通過(guò) send
方法轉(zhuǎn)移給接收端。接收端通過(guò) recv
方法接收數(shù)據(jù)。
2.2 發(fā)送多個(gè)值并觀察接收者的等待
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();// 創(chuàng)建一個(gè)線程發(fā)送多個(gè)值let sender_thread = thread::spawn(move || {for i in 1..=5 {tx.send(i).unwrap();println!("Sent {}", i);}});// 創(chuàng)建一個(gè)線程接收值let receiver_thread = thread::spawn(move || {for _ in 1..=5 {let received = rx.recv().unwrap();println!("Received {}", received);}});sender_thread.join().unwrap();receiver_thread.join().unwrap();
}
運(yùn)行結(jié)果
Sent 1
Sent 2
Sent 3
Sent 4
Sent 5
Received 1
Received 2
Received 3
Received 4
Received 5
在這個(gè)示例中,我們創(chuàng)建了一個(gè)線程發(fā)送一系列值(1 到 5),并在發(fā)送每個(gè)值后打印一條消息。接收端在接收到所有值之前會(huì)阻塞等待。
2.3 通過(guò)克隆發(fā)送者來(lái)創(chuàng)建多個(gè)生產(chǎn)者
use std::sync::mpsc;
use std::thread;fn main() {let (tx, rx) = mpsc::channel();// 克隆發(fā)送端以創(chuàng)建多個(gè)生產(chǎn)者let tx_clone = tx.clone();let sender_thread1 = thread::spawn(move || {for i in 1..=5 {tx.send(i).unwrap();println!("Sender 1 sent {}", i);}});let sender_thread2 = thread::spawn(move || {for i in 5..=10 {tx_clone.send(i).unwrap();println!("Sender 2 sent {}", i);}});// 接收端let receiver_thread = thread::spawn(move || {let mut values = Vec::new();for _ in 1..=10 {values.push(rx.recv().unwrap());}println!("Received all values: {:?}", values);});sender_thread1.join().unwrap();sender_thread2.join().unwrap();receiver_thread.join().unwrap();
}
運(yùn)行結(jié)果
Sender 1 sent 1
Sender 1 sent 2
Sender 2 sent 5
Sender 2 sent 6
Sender 2 sent 7
Sender 2 sent 8
Sender 2 sent 9
Sender 2 sent 10
Sender 1 sent 3
Sender 1 sent 4
Sender 1 sent 5
Received all values: [1, 5, 2, 6, 7, 8, 9, 10, 3, 4]
在這個(gè)示例中,我們克隆了發(fā)送端 tx
來(lái)創(chuàng)建兩個(gè)生產(chǎn)者 sender_thread1
和 sender_thread2
。每個(gè)生產(chǎn)者都發(fā)送一系列值,而接收端接收所有值并將它們存儲(chǔ)在一個(gè)向量中。
請(qǐng)注意,在實(shí)際應(yīng)用中,你可能需要考慮使用更高級(jí)的并發(fā)原語(yǔ),如 std::sync::Arc
來(lái)共享狀態(tài),或者使用 std::sync::Barrier
來(lái)同步線程。此外,錯(cuò)誤處理在多線程程序中也非常重要,這里為了示例的簡(jiǎn)潔性,我們使用了 unwrap
來(lái)處理可能的錯(cuò)誤。在生產(chǎn)代碼中,你應(yīng)該更細(xì)致地處理這些情況。
三、共享狀態(tài)
在某種程度上,任何編程語(yǔ)言中的信道都類似于單所有權(quán),因?yàn)橐坏⒁粋€(gè)值傳送到信道中,將無(wú)法再使用這個(gè)值。共享內(nèi)存類似于多所有權(quán):多個(gè)線程可以同時(shí)訪問(wèn)相同的內(nèi)存位置。
互斥鎖
互斥鎖(mutex)是 mutual exclusion 的縮寫(xiě),也就是說(shuō),任意時(shí)刻,其只允許一個(gè)線程訪問(wèn)某些數(shù)據(jù)。為了訪問(wèn)互斥鎖中的數(shù)據(jù),線程首先需要通過(guò)獲取互斥鎖的鎖(lock)來(lái)表明其希望訪問(wèn)數(shù)據(jù)。鎖是一個(gè)作為互斥鎖一部分的數(shù)據(jù)結(jié)構(gòu),它記錄誰(shuí)有數(shù)據(jù)的排他訪問(wèn)權(quán)。因此,我們描述互斥鎖為通過(guò)鎖系統(tǒng)保護(hù)(guarding)其數(shù)據(jù)。
互斥鎖以難以使用著稱,因?yàn)槟悴坏貌挥涀?#xff1a;
- 在使用數(shù)據(jù)之前嘗試獲取鎖。
- 處理完被互斥鎖所保護(hù)的數(shù)據(jù)之后,必須解鎖數(shù)據(jù),這樣其他線程才能夠獲取鎖。
正確的管理互斥鎖異常復(fù)雜,這也是許多人之所以熱衷于信道的原因。然而,在 Rust 中,得益于類型系統(tǒng)和所有權(quán),我們不會(huì)在鎖和解鎖上出錯(cuò)。
Mutex<T>
是 std::sync
模塊提供的一種同步原語(yǔ),用于在多線程環(huán)境中保護(hù)共享數(shù)據(jù)。Mutex<T>
代表互斥鎖(Mutex),它允許你以線程安全的方式對(duì)數(shù)據(jù)進(jìn)行訪問(wèn)。以下是 Mutex<T>
的 API 詳細(xì)介紹:
構(gòu)造函數(shù)
Mutex::new(data: T)
: 創(chuàng)建一個(gè)新的Mutex
,并將data
作為內(nèi)部數(shù)據(jù)。
方法
lock()
: 獲取互斥鎖,返回一個(gè)可變引用MutexGuard
。如果鎖當(dāng)前被其他線程持有,則當(dāng)前線程將阻塞,直到鎖被釋放。try_lock()
: 嘗試獲取互斥鎖,如果成功則返回Some(MutexGuard)
,如果失敗(鎖已被其他線程持有)則返回None
,不會(huì)阻塞當(dāng)前線程。get_mut()
: 返回對(duì)內(nèi)部可變數(shù)據(jù)的可變引用。這個(gè)方法需要Mutex
已經(jīng)獲得鎖,通常在使用lock
或try_lock
之后使用。into_inner()
: 消耗Mutex
并返回內(nèi)部數(shù)據(jù)。這個(gè)方法只有在Mutex
沒(méi)有被鎖定的情況下才能成功使用,否則會(huì) panic。
實(shí)例方法
is_poisoned()
: 檢查互斥鎖是否處于“中毒”狀態(tài)。如果一個(gè)線程在持有互斥鎖時(shí) panic,那么鎖將進(jìn)入中毒狀態(tài)。其他線程在嘗試獲取這個(gè)鎖時(shí)會(huì)收到一個(gè)PoisonError
。unlock()
: 釋放互斥鎖。通常,互斥鎖會(huì)在MutexGuard
離開(kāi)作用域時(shí)自動(dòng)釋放,但在某些情況下,你可能需要手動(dòng)釋放鎖。
類型
MutexGuard<'a, T>
: 表示對(duì)互斥鎖的鎖定訪問(wèn)。它是通過(guò)調(diào)用lock
或try_lock
獲得的。當(dāng)MutexGuard
離開(kāi)作用域時(shí),互斥鎖會(huì)自動(dòng)釋放。
錯(cuò)誤處理
PoisonError<T>
: 當(dāng)互斥鎖中毒時(shí),lock
和try_lock
方法會(huì)返回這個(gè)錯(cuò)誤。它包含內(nèi)部數(shù)據(jù)的一個(gè)可變引用,允許你安全地處理中毒情況。
當(dāng)然,這里有一個(gè)使用 Mutex<T>
的 Rust 程序示例,它演示了如何在多個(gè)線程之間共享和修改數(shù)據(jù):
use std::sync::{Arc, Mutex};
use std::thread;fn main() {// 創(chuàng)建一個(gè)Arc包裝的Mutex,以便跨多個(gè)線程安全共享let counter = Arc::new(Mutex::new(0));let mut handles = vec![];// 創(chuàng)建并啟動(dòng)10個(gè)線程for _ in 0..10 {// 克隆Arc來(lái)獲取counter的一個(gè)新引用let counter_clone = Arc::clone(&counter);let handle = thread::spawn(move || {// 通過(guò)lock獲取互斥鎖的訪問(wèn)權(quán)let mut num = counter_clone.lock().unwrap();// 修改數(shù)據(jù)*num += 1;});handles.push(handle);}// 等待所有線程完成for handle in handles {handle.join().unwrap();}// 打印最終的計(jì)數(shù)結(jié)果println!("Result: {}", *counter.lock().unwrap());
}
運(yùn)行結(jié)果
Result: 10
這個(gè)程序執(zhí)行以下步驟:
- 使用
Arc::new
和Mutex::new
創(chuàng)建一個(gè)在多個(gè)線程間共享的計(jì)數(shù)器。 - 創(chuàng)建一個(gè)線程向量
handles
來(lái)存儲(chǔ)所有線程的句柄。 - 在一個(gè)循環(huán)中,為每個(gè)線程克隆
Arc
對(duì)象,以確保每個(gè)線程都有counter
的獨(dú)立引用。 - 每個(gè)線程嘗試獲取
Mutex
的鎖,遞增計(jì)數(shù)器的值,然后釋放鎖。 - 使用
thread::spawn
啟動(dòng)每個(gè)線程。 - 在主線程中,等待所有子線程完成。
- 打印出最終的計(jì)數(shù)結(jié)果。
這個(gè)程序使用了 Arc
(Atomic Reference Counting) 來(lái)允許 Mutex
在多個(gè)線程間安全共享。Arc::clone
用于增加內(nèi)部引用計(jì)數(shù),而不是復(fù)制數(shù)據(jù)。每個(gè)線程通過(guò)調(diào)用 lock
方法來(lái)獲取互斥鎖的可變?cè)L問(wèn)權(quán),并通過(guò) unwrap
處理可能的鎖定錯(cuò)誤(在實(shí)際應(yīng)用中,你可能需要更優(yōu)雅地處理這種錯(cuò)誤)。當(dāng) MutexGuard
離開(kāi)作用域時(shí),互斥鎖會(huì)自動(dòng)釋放。
四、使用 Sync 和 Send trait 的可擴(kuò)展并發(fā)
之前討論的幾乎所有內(nèi)容,都屬于標(biāo)準(zhǔn)庫(kù),而不是語(yǔ)言本身的內(nèi)容。由于不需要語(yǔ)言提供并發(fā)相關(guān)的基礎(chǔ)設(shè)施,并發(fā)方案不受標(biāo)準(zhǔn)庫(kù)或語(yǔ)言所限:我們可以編寫(xiě)自己的或使用別人編寫(xiě)的并發(fā)功能。
然而有兩個(gè)并發(fā)概念是內(nèi)嵌于語(yǔ)言中的:std::marker
中的 Sync
和 Send
trait。
4.1 通過(guò) Send 允許在線程間轉(zhuǎn)移所有權(quán)
Send
標(biāo)記 trait 表明實(shí)現(xiàn)了 Send
的類型值的所有權(quán)可以在線程間傳送。幾乎所有的 Rust 類型都是 Send
的,不過(guò)有一些例外,包括 Rc<T>
:這是不能 Send
的,因?yàn)槿绻寺×?Rc<T>
的值并嘗試將克隆的所有權(quán)轉(zhuǎn)移到另一個(gè)線程,這兩個(gè)線程都可能同時(shí)更新引用計(jì)數(shù)。為此,Rc<T>
被實(shí)現(xiàn)為用于單線程場(chǎng)景,這時(shí)不需要為擁有線程安全的引用計(jì)數(shù)而付出性能代價(jià)。
因此,Rust 類型系統(tǒng)和 trait bound 確保永遠(yuǎn)也不會(huì)意外的將不安全的 Rc<T>
在線程間發(fā)送。而使用標(biāo)記為 Send
的 Arc<T>
時(shí),就沒(méi)有問(wèn)題了。
任何完全由 Send
的類型組成的類型也會(huì)自動(dòng)被標(biāo)記為 Send
。幾乎所有基本類型都是 Send
的,除了裸指針(raw pointer)。
4.2 Sync 允許多線程訪問(wèn)
Sync
標(biāo)記 trait 表明一個(gè)實(shí)現(xiàn)了 Sync
的類型可以安全的在多個(gè)線程中擁有其值的引用。換一種方式來(lái)說(shuō),對(duì)于任意類型 T
,如果 &T
(T
的不可變引用)是 Send
的話 T
就是 Sync
的,這意味著其引用就可以安全的發(fā)送到另一個(gè)線程。類似于 Send
的情況,基本類型是 Sync
的,完全由 Sync
的類型組成的類型也是 Sync
的。
智能指針 Rc<T>
也不是 Sync
的,出于其不是 Send
相同的原因。RefCell<T>
和 Cell<T>
系列類型不是 Sync
的。RefCell<T>
在運(yùn)行時(shí)所進(jìn)行的借用檢查也不是線程安全的。Mutex<T>
是 Sync
的。
4.3 手動(dòng)實(shí)現(xiàn) Send 和 Sync 是不安全的
通常并不需要手動(dòng)實(shí)現(xiàn) Send
和 Sync
trait,因?yàn)橛?Send
和 Sync
的類型組成的類型,自動(dòng)就是 Send
和 Sync
的。因?yàn)樗鼈兪菢?biāo)記 trait,甚至都不需要實(shí)現(xiàn)任何方法。它們只是用來(lái)加強(qiáng)并發(fā)相關(guān)的不可變性的。
手動(dòng)實(shí)現(xiàn)這些標(biāo)記 trait 涉及到編寫(xiě)不安全的 Rust 代碼,當(dāng)前重要的是,在創(chuàng)建新的由不是 Send
和 Sync
的部分構(gòu)成的并發(fā)類型時(shí)需要多加小心,以確保維持其安全保證。
以下是一個(gè)完整的示例,展示如何在一個(gè)多線程程序中手動(dòng)實(shí)現(xiàn) Send
和 Sync
,以及如何在多個(gè)線程之間共享數(shù)據(jù)。
首先,我們定義一個(gè)簡(jiǎn)單的結(jié)構(gòu)體 SharedData
,它包含了一些數(shù)據(jù),我們將為這個(gè)結(jié)構(gòu)體手動(dòng)實(shí)現(xiàn) Send
和 Sync
特征。
use std::sync::{Arc, Mutex};
use std::thread;// 定義一個(gè)簡(jiǎn)單的結(jié)構(gòu)體,包含一些數(shù)據(jù)
struct SharedData {count: i32,
}// 手動(dòng)實(shí)現(xiàn)Send特征,表示SharedData可以安全地在線程間發(fā)送
unsafe impl Send for SharedData {}// 手動(dòng)實(shí)現(xiàn)Sync特征,表示SharedData可以被多個(gè)線程安全地訪問(wèn)
unsafe impl Sync for SharedData {}fn main() {// 使用Arc來(lái)包裝SharedData,使其可以被多個(gè)線程共享let shared_data = Arc::new(Mutex::new(SharedData { count: 0 }));// 創(chuàng)建多個(gè)線程,它們將共享并修改同一個(gè)SharedData實(shí)例let mut handles = vec![];for i in 0..10 {let data = Arc::clone(&shared_data);let handle = thread::spawn(move || {let mut data = data.lock().unwrap();println!("i: {}", i);data.count += i;});handles.push(handle);}// 等待所有線程完成for handle in handles {handle.join().unwrap();}// 打印最終的count值println!("Final count is: {}", shared_data.lock().unwrap().count);
}
運(yùn)行結(jié)果
i: 1
i: 0
i: 2
i: 9
i: 8
i: 4
i: 3
i: 5
i: 7
i: 6
Final count is: 45
在這個(gè)例子中,我們首先定義了一個(gè) SharedData
結(jié)構(gòu)體,它包含一個(gè) i32
類型的字段 count
。然后我們手動(dòng)為 SharedData
實(shí)現(xiàn)了 Send
和 Sync
特征,使用 unsafe
關(guān)鍵字,因?yàn)槲覀冃枰_保 SharedData
在多線程環(huán)境中是安全的。
在 main
函數(shù)中,我們使用 Arc
(原子引用計(jì)數(shù)指針)和 Mutex
(互斥鎖)來(lái)創(chuàng)建一個(gè)可以在多個(gè)線程之間安全共享的 SharedData
實(shí)例。Arc
允許多個(gè)線程擁有對(duì)數(shù)據(jù)的所有權(quán),而 Mutex
確保在任何時(shí)刻只有一個(gè)線程可以修改數(shù)據(jù)。
我們創(chuàng)建了 10 個(gè)線程,每個(gè)線程都會(huì)增加 SharedData
中的 count
字段的值。由于我們?yōu)?SharedData
實(shí)現(xiàn)了 Send
和 Sync
,我們可以安全地將 Arc<Mutex<SharedData>>
的克隆傳遞給每個(gè)線程。每個(gè)線程完成其工作后,我們使用 join
方法等待所有線程完成,并打印最終的 count
值。
參考鏈接
- Rust 官方網(wǎng)站:https://www.rust-lang.org/zh-CN
- Rust 官方文檔:https://doc.rust-lang.org/
- Rust Play:https://play.rust-lang.org/
- 《Rust 程序設(shè)計(jì)語(yǔ)言》