並行
並行(concurrency)程式讓多件事同時進行,但也最容易出現難以除錯的資料競爭(data race)。Rust 的所有權與型別系統能在編譯時擋下這類錯誤,這被稱為「無畏並行」(fearless concurrency)。本頁介紹執行緒、通道、共享狀態,並簡述 async。
執行緒
用 thread::spawn 建立新執行緒,回傳的 JoinHandle 可以呼叫 join() 等它結束。
use std::thread;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=3 {
println!("子執行緒:{i}");
}
});
println!("主執行緒繼續執行");
handle.join().unwrap(); // 等子執行緒完成
}
輸出的順序不固定,取決於作業系統排程。
用 move 把資料交給執行緒
新執行緒可能比建立它的函式活得更久,因此不能借用區域變數,必須用 move 取走所有權:
use std::thread;
fn main() {
let numbers = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("子執行緒拿到:{:?}", numbers);
});
// println!("{:?}", numbers); // 編譯錯誤:已經被移進執行緒
handle.join().unwrap();
}
通道:用傳訊息溝通
通道(channel)讓執行緒之間傳遞訊息,而不是共享記憶體。mpsc 代表「多個生產者、單一消費者」(multiple producer, single consumer):
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
for id in 1..=3 {
let tx = tx.clone(); // 每個生產者各有一個發送端
thread::spawn(move || {
tx.send(format!("工人 {id} 完成")).unwrap();
});
}
drop(tx); // 關閉原本的發送端,接收端才知道何時結束
let mut results: Vec<String> = rx.iter().collect();
results.sort();
for r in results {
println!("{r}");
}
}
send 會移動訊息的所有權,所以送出後發送方就不能再使用它,避免兩邊同時修改。
共享狀態:Arc 與 Mutex
多個執行緒需要讀寫同一份資料時,使用:
Mutex<T>:互斥鎖,同一時間只有一個執行緒能取得資料。Arc<T>:原子參照計數(atomic reference counting),是可跨執行緒使用的Rc<T>。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
})); // num 離開作用域時自動解鎖
}
for h in handles {
h.join().unwrap();
}
println!("結果:{}", *counter.lock().unwrap());
}
lock() 回傳的守衛(guard)離開作用域時會自動解鎖,不用手動 unlock,也不會忘記。
為什麼編譯器能擋下資料競爭?
Rust 用兩個標記 trait 描述型別能否跨執行緒使用:
| Trait | 意義 |
|---|---|
Send |
這個型別的所有權可以轉移到另一個執行緒 |
Sync |
這個型別可以被多個執行緒同時以參照存取 |
Rc<T> 沒有實作 Send,所以把它丟進 thread::spawn 會編譯失敗,必須改用 Arc<T>。你不需要手動處理這些 trait,編譯器會自動推導並檢查。
async 簡介
上面的執行緒模型適合運算密集的工作。若程式要同時處理大量網路連線這類等待多於運算的 I/O,開一堆執行緒成本很高,此時會用 async/await:
要注意兩件事:
async fn回傳的是 Future,本身不會執行,必須有人去「驅動」它。- Rust 標準函式庫不含執行器(runtime),需要引入 Tokio 之類的套件才能實際執行 async 程式碼。
初學階段不必急著學 async,先把執行緒、所有權與錯誤處理學好,再依需求(例如寫網路服務)學習。
該選哪一種?
| 情況 | 做法 |
|---|---|
| 運算密集的獨立任務 | thread::spawn |
| 執行緒間傳遞資料 | 通道 mpsc |
| 多個執行緒共同讀寫同一份資料 | Arc<Mutex<T>> |
| 大量網路 I/O | async/await 加上 Tokio 等 runtime |
推薦影音
建立執行緒
簡述:Let's Get Rusty 依照官方書籍第 16 章製作,介紹 thread::spawn、join 與 move 閉包。
訊息傳遞
簡述:同一系列的通道單元,介紹 mpsc 的發送端與接收端,對應本頁「通道」一節。
共享狀態
簡述:同一系列的共享狀態單元,介紹 Mutex 與 Arc,對應本頁「共享狀態」一節。
async 入門
簡述:Let's Get Rusty 用約 20 分鐘說明 Rust 的 async 如何運作,適合在學完執行緒後,想了解 async/await 的人。