Rust 並行與多執行緒 ── 執行緒(Threads)基礎
簡介
在現代硬體上,CPU 常常擁有多顆核心(core),只有善用 多執行緒,才能真正發揮硬體的運算效能。
對於 系統程式、伺服器、或是 資料處理 等需要同時處理多項工作、降低延遲的應用而言,掌握執行緒的使用是必備技能。
Rust 以 所有權(ownership) 與 借用檢查(borrow checker) 為核心,提供了安全且高效的執行緒模型。即使是初學者,也能在不犧牲安全性的前提下,寫出可靠的並行程式。
本文將從 建立執行緒、資料共享、同步機制 等基礎概念出發,搭配實用範例,說明在 Rust 中如何正確且有效地使用執行緒。
核心概念
1. std::thread 模組與 thread::spawn
Rust 標準函式庫的 std::thread 提供了最直接的執行緒 API。thread::spawn 會接受一個 閉包(closure),在新執行緒中執行,並回傳 JoinHandle<T>,其中 T 為閉包的返回值類型。
use std::thread;
fn main() {
// 建立一個執行緒,印出訊息後自動結束
let handle = thread::spawn(|| {
println!("Hello from a new thread!");
});
// 主執行緒繼續執行
println!("Hello from the main thread!");
// 等待子執行緒結束
handle.join().unwrap();
}
重點:
join必須被呼叫,否則子執行緒可能在主執行緒結束前被強制終止,導致資源未正確釋放。
2. 傳遞資料給執行緒
由於 Rust 的所有權規則,閉包會 捕獲它所使用的變數。
如果要把資料移交給執行緒,必須使用 move 關鍵字,將所有權搬移(move)到子執行緒。
use std::thread;
fn main() {
let data = vec![1, 2, 3, 4, 5];
// 使用 move 把 data 的所有權搬移到新執行緒
let handle = thread::spawn(move || {
// 此處 data 已經是子執行緒的所有者
let sum: i32 = data.iter().sum();
println!("Sum in thread: {}", sum);
});
// 主執行緒此時已無法再使用 data
// println!("{:?}", data); // 編譯錯誤
handle.join().unwrap();
}
如果需要在多個執行緒之間共享同一筆資料,則必須使用 同步原語(例如 Arc、Mutex)來保證資料安全。
3. Arc(Atomic Reference Counted)與 Mutex
Arc<T>:多執行緒環境下的引用計數指標,允許多個執行緒共享同一筆資料的所有權。Mutex<T>:互斥鎖,確保同一時間只有一個執行緒能夠存取被保護的資料。
下面的範例示範如何使用 Arc<Mutex<T>> 讓多個執行緒安全地累加計數器:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 建立一個被 Mutex 包住的計數器,並用 Arc 共享所有權
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
// 為每個執行緒克隆一個 Arc(增加引用計數)
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 取得鎖,執行臨界區操作
let mut num = counter_clone.lock().unwrap();
*num += 1;
// 鎖在作用域結束時自動釋放
});
handles.push(handle);
}
// 等所有執行緒結束
for h in handles {
h.join().unwrap();
}
println!("Final counter: {}", *counter.lock().unwrap());
}
技巧:
Arc::clone(&counter)只會複製指標,成本極低;真正的資料仍只有一份。
4. channel(訊道)進行執行緒間通訊
有時候不需要共享可變資料,而是希望執行緒之間以 訊息傳遞 的方式協調工作。
Rust 提供了 std::sync::mpsc(multiple producer, single consumer)通道。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// 建立一個傳送端 (tx) 與接收端 (rx)
let (tx, rx) = mpsc::channel();
// 產生多個生產者執行緒
for i in 0..5 {
let tx_clone = tx.clone(); // 每個執行緒都有自己的傳送端
thread::spawn(move || {
thread::sleep(Duration::from_millis(50 * i));
tx_clone.send(format!("Message from thread {}", i)).unwrap();
});
}
// 主執行緒作為唯一的消費者
for _ in 0..5 {
let msg = rx.recv().unwrap(); // 阻塞等待訊息
println!("Received: {}", msg);
}
}
此範例展示 多生產者單消費者 的模式,適合「工作者池」或「事件驅動」的情境。
5. thread::sleep 與 JoinHandle::join
thread::sleep:讓目前執行緒暫停指定時間,常用於模擬 I/O 或測試併發行為。JoinHandle::join:等待執行緒結束並取得其返回值。若執行緒 panic,join會回傳Err.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
thread::sleep(Duration::from_secs(2));
42 // 執行緒的返回值
});
println!("Waiting for the thread...");
match handle.join() {
Ok(v) => println!("Thread finished with value {}", v),
Err(e) => eprintln!("Thread panicked: {:?}", e),
}
}
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
忘記 move |
閉包捕獲的變數仍屬於主執行緒,會導致編譯錯誤或資料競爭。 | 在 thread::spawn 時使用 move,明確搬移所有權。 |
| 資料競爭(Data Race) | 多執行緒同時寫同一變數,未使用同步原語。 | 使用 Arc<Mutex<T>> 或 RwLock<T> 包裝共享資料。 |
| 死鎖(Deadlock) | 多把鎖以不同順序取得,導致相互等待。 | 統一鎖的取得順序,或使用 try_lock 失敗時回退。 |
| 過度產生執行緒 | 每個任務都建立新執行緒,系統資源耗盡。 | 使用 執行緒池(如 rayon、tokio)或 工作者模型。 |
忘記 join |
主執行緒提前結束,子執行緒被強制終止。 | 在需要時 呼叫 handle.join(),或使用 scope(crossbeam)確保生命週期。 |
最佳實踐
- 盡量使用不可變共享:如果資料不需要變更,使用
Arc<T>(無Mutex)即可,避免不必要的鎖開銷。 - 限制鎖的範圍:只在必要的程式區段持有鎖,減少阻塞時間。
- 錯誤處理:
join、lock、send、recv都可能失敗,務必使用Result處理。 - 測試與偵錯:利用
cargo test -- --nocapture觀察執行緒輸出,或使用RUST_BACKTRACE=1追蹤 panic。 - 考慮使用高階抽象:對於複雜的併發需求,
rayon(資料平行)或tokio(非阻塞 async)能提供更安全、更高效的實作。
實際應用場景
| 場景 | 為何需要執行緒 | 典型實作方式 |
|---|---|---|
| Web 伺服器 | 同時處理多個請求、避免阻塞 I/O | 使用 tokio 的非阻塞執行緒或 rayon 處理 CPU 密集工作 |
| 資料批次處理 | 大量檔案或資料分割後平行計算 | Arc<Mutex<Vec<T>>> + 多執行緒累加結果,或 rayon::par_iter |
| 即時遊戲伺服器 | 網路訊息、物理模擬、AI 同時運算 | 工作者池(thread pool)分配不同任務,使用訊道傳遞結果 |
| 硬體驅動 / 嵌入式 | 中斷處理與背景任務分離 | std::thread::spawn 搭配 Mutex/Condvar 進行資源同步 |
| 日誌與監控 | 高頻率寫入檔案或遠端服務,避免阻塞主流程 | 背景執行緒使用 channel 收集訊息,批次寫入 |
總結
- 執行緒是 Rust 並行程式設計的基礎,透過
thread::spawn、Arc、Mutex、channel等工具,我們可以在不犧牲安全性的前提下,寫出高效能的多執行緒程式。 - 理解 所有權搬移(move)、資料共享 以及 同步機制,是避免資料競爭與死鎖的關鍵。
- 在實務開發中,適度使用執行緒池或高階抽象(如
rayon、tokio)可以降低程式碼複雜度,同時提升可維護性與效能。
掌握了本文的概念與範例,你就能在 Rust 中自信地運用執行緒,為各種 CPU 密集、I/O 密集 或 即時 的應用提供穩定且高效的併發解決方案。祝你寫程式順利,玩得開心! 🚀