本文 AI 產出,尚未審核
Rust 並行與多執行緒:使用 spawn 建立執行緒
簡介
在現代硬體上,CPU 核心數量持續增加,單一執行緒已無法充分發揮效能。並行程式設計成為提升效能、改善使用者體驗的關鍵。Rust 以「安全」為核心,提供了零成本抽象的執行緒模型,使開發者能在不犧牲安全性的前提下,輕鬆利用多核心。
std::thread::spawn 是 Rust 建立新執行緒的最直接方式。透過它,我們可以把一段閉包 (closure) 交給作業系統排程,與主執行緒同步或非同步執行。本文將從概念、實作、常見陷阱到實務應用,完整說明如何在 Rust 中使用 spawn。
核心概念
1. spawn 的基本語法
use std::thread;
fn main() {
// 建立一個新執行緒,執行閉包內的程式碼
let handle = thread::spawn(|| {
println!("我是子執行緒!");
});
// 主執行緒繼續執行
println!("我是主執行緒!");
// 等待子執行緒結束
handle.join().unwrap();
}
thread::spawn會回傳JoinHandle<T>,T為閉包的回傳型別。join()會阻塞當前執行緒,直到子執行緒結束,並傳回子執行緒的結果或 panic 訊息。
2. 傳遞資料給子執行緒
Rust 的所有權規則仍然適用。若要將資料搬移 (move) 給子執行緒,必須使用 move 關鍵字:
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4];
let handle = thread::spawn(move || {
// data 已被搬移到此閉包內
println!("子執行緒取得資料: {:?}", data);
});
// 這裡已無法再使用 data
handle.join().unwrap();
}
- 搬移 (
move) 會將資料的所有權從主執行緒轉移到子執行緒,避免同時存取產生競爭條件。 - 若需要共享資料,請改用 同步原語(如
Arc、Mutex)。
3. 使用 Arc + Mutex 共享可變資料
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 使用 Arc 讓多個執行緒共享所有權,Mutex 保障可變存取的安全
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let c = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 取得鎖定,修改資料
let mut num = c.lock().unwrap();
*num += 1;
println!("執行緒內部計數: {}", *num);
});
handles.push(handle);
}
// 等待所有子執行緒結束
for h in handles {
h.join().unwrap();
}
println!("最終計數: {}", *counter.lock().unwrap());
}
Arc<T>(Atomic Reference Counted)允許跨執行緒共享所有權。Mutex<T>提供互斥鎖,確保同時間只有一個執行緒能修改資料。- 這樣的組合是 Rust 中最常見的共享可變狀態 實作方式。
4. 取得子執行緒的回傳值
spawn 允許閉包回傳任意型別,只要在 join() 時正確處理即可:
use std::thread;
fn main() {
let handle = thread::spawn(|| -> usize {
// 計算 1~100 的總和
(1..=100).sum()
});
// join() 會回傳 Result<T, Box<dyn Any + Send + 'static>>
let sum = handle.join().expect("執行緒發生 panic");
println!("1~100 的總和是 {}", sum);
}
- 若子執行緒 panic,
join()會回傳Err,必須使用expect、unwrap或自行處理錯誤。
5. 限制執行緒數量:使用 ThreadPool
直接呼叫 spawn 會為每個任務建立新執行緒,過多執行緒會耗盡系統資源。實務上常使用 執行緒池(ThreadPool):
use std::sync::mpsc;
use std::thread;
fn main() {
// 建立一個簡易的執行緒池,固定 4 個執行緒
let (tx, rx) = mpsc::channel();
let mut workers = vec![];
for id in 0..4 {
let rx = rx.clone();
let worker = thread::spawn(move || {
while let Ok(job) = rx.recv() {
println!("執行緒 {} 處理工作: {}", id, job);
// 模擬工作
thread::sleep(std::time::Duration::from_millis(200));
}
});
workers.push(worker);
}
// 發送 10 個工作項目
for i in 0..10 {
tx.send(format!("工作 {}", i)).unwrap();
}
drop(tx); // 關閉通道,讓工作者結束迴圈
for w in workers {
w.join().unwrap();
}
}
- 這裡使用
mpsc(multiple producer, single consumer)通道在主執行緒與工作執行緒之間傳遞任務。 - 真正的專案建議使用
rayon、tokio或async-std等成熟庫,本文僅示範概念。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 |
|---|---|---|
忘記 move |
編譯錯誤:閉包捕獲的變數無法跨執行緒使用 | 在 spawn 的閉包前加 move,或使用 Arc/Mutex |
| 共享可變資料未加鎖 | 資料競爭、未定義行為、程式 panic | 使用 Arc<Mutex<T>> 或 RwLock,確保同時只有一個執行緒寫入 |
| 過度產生執行緒 | 系統資源耗盡、效能下降 | 使用執行緒池或限制同時執行的執行緒數量 |
忽略 join 的錯誤 |
子執行緒 panic 但主執行緒仍繼續,可能導致不一致狀態 | 使用 handle.join().expect("子執行緒失敗"),或自行處理 Result |
跨執行緒傳遞非 Send 類型 |
編譯錯誤:T 必須實作 Send |
確認要傳遞的資料類型實作 Send(大多數標準型別已支援) |
最佳實踐
- 盡量使用
move+Arc/Mutex:保證所有權清晰,避免隱藏的資料競爭。 - 限制執行緒數量:根據 CPU 核心數 (
num_cpus::get()) 設定上限,或使用成熟的執行緒池庫。 - 錯誤處理:
join()必須檢查Result,避免 silent panic。 - 避免長時間阻塞:若執行緒內部有 I/O,考慮使用非同步 runtime(如
tokio)取代傳統執行緒。 - 測試與 profiling:使用
cargo bench、perf或flamegraph觀察執行緒的實際效能。
實際應用場景
| 場景 | 為何使用 spawn |
範例簡述 |
|---|---|---|
| 網路爬蟲 | 同時抓取多個網站,提升下載速度 | 為每個 URL 建立子執行緒,使用 join 收集結果 |
| 圖像處理 | 大量像素運算可平行化 | 把圖像切割成多塊,交給不同執行緒計算,最後合併 |
| 日誌寫入 | 高頻率寫入不阻塞主流程 | 建立單一寫入執行緒,主執行緒透過 channel 發送 log 訊息 |
| 資料庫批次寫入 | 多筆寫入同時進行,減少等待時間 | 使用執行緒池分配寫入工作,提升吞吐量 |
| 遊戲伺服器 | 處理多玩家的即時指令 | 每個玩家的指令交給獨立執行緒或工作池,保持低延遲 |
小技巧:在 CPU 密集型工作時,
thread::available_parallelism()(Rust 1.59+)可取得系統建議的平行度,作為建立執行緒數量的參考。
總結
std::thread::spawn是 Rust 建立執行緒的核心 API,配合move、Arc、Mutex能安全地在多執行緒環境中共享與修改資料。- 正確處理所有權、錯誤與資源限制,是避免競爭條件與效能瓶頸的關鍵。
- 實務上,執行緒池、非同步 runtime 與 適當的同步原語 結合,能讓程式在多核心機器上發揮最大效能。
- 只要掌握上述概念與最佳實踐,從簡單的
spawn到複雜的並行演算法,Rust 都能提供「零成本抽象」的安全保證。
祝你在 Rust 的並行世界中寫出高效、可靠的程式碼! 🚀