Rust 並行與多執行緒:共享狀態(Mutex、Arc)
簡介
在多執行緒程式中,共享狀態是最常見也最容易出錯的部分。Rust 以所有權與借用檢查為核心,天然防止了資料競爭(data race),但當多個執行緒真的需要同時讀寫同一筆資料時,我們必須使用同步原語來明確表達「此時此刻只能有一個執行緒取得寫入權限」。
Mutex(mutual exclusion)提供了互斥鎖,保證同一時間只有一個執行緒可以存取被保護的資料;而 Arc(atomic reference counted)則是 原子化的引用計數,讓多個執行緒可以安全地共享同一個 Mutex(或其他 Sync 資源)。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步掌握在 Rust 中安全且高效地共享狀態。
核心概念
1. Mutex:互斥鎖的基本使用
Mutex<T> 包裝了一個 T,並在每次存取時要求呼叫 .lock() 取得 MutexGuard<T>。MutexGuard 在離開作用域時會自動釋放鎖,符合 RAII(Resource Acquisition Is Initialization)原則。
use std::sync::Mutex;
fn main() {
// 建立一個被 Mutex 保護的計數器
let counter = Mutex::new(0);
// 取得鎖,得到可變的參考
{
let mut num = counter.lock().unwrap(); // unwrap 會在 lock 被 poison 時 panic
*num += 1; // 直接修改內部資料
} // 這裡 num 被 drop,鎖自動釋放
println!("counter = {}", *counter.lock().unwrap());
}
重點:
Mutex本身 不會自動 在多執行緒間共享,我們仍需要一個可以跨執行緒傳遞的指標(如Arc)才能真正做到共享。
2. Arc:跨執行緒的引用計數
Arc<T> 與單執行緒環境下的 Rc<T> 類似,但它的引用計數是 原子操作,因此可以安全地在多執行緒間 clone。
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
// 複製 Arc,產生另一個指向相同資料的所有權
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// 在子執行緒中使用 data_clone
println!("子執行緒看到的資料: {:?}", data_clone);
});
// 主執行緒仍然可以使用 data
println!("主執行緒看到的資料: {:?}", data);
handle.join().unwrap();
}
注意:
Arc<T>只提供 共享(&T)的存取權限,若要 可變 存取,必須再搭配Mutex(或RwLock)使用。
3. Arc<Mutex<T>>:共享可變狀態的典型組合
最常見的共享可變狀態寫法是 Arc<Mutex<T>>。Arc 讓多個執行緒可以持有同一個 Mutex,而 Mutex 則保證同一時間只有一個執行緒能修改 T。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// 共享的計數器,初始值為 0
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
// 為每個執行緒 clone 一個 Arc
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 每個執行緒執行 100 次遞增
for _ in 0..100 {
let mut num = counter_clone.lock().unwrap();
*num += 1;
// MutexGuard 在此作用域結束時自動釋放
}
});
handles.push(handle);
}
// 等待所有執行緒結束
for h in handles {
h.join().unwrap();
}
println!("最終計數結果 = {}", *counter.lock().unwrap());
}
關鍵:
Arc::clone只複製指標,不會 複製底層資料;所有執行緒仍然操作同一個Mutex包住的值。
4. MutexGuard 與作用域管理
MutexGuard<T> 實作了 Deref 與 DerefMut,因此我們可以像操作 &mut T 一樣使用它。但要特別留意 作用域,因為只要 MutexGuard 仍在,鎖就不會釋放。
use std::sync::{Arc, Mutex};
fn heavy_computation(data: &mut Vec<u8>) {
// 假裝做很重的計算
data.push(42);
}
fn main() {
let shared = Arc::new(Mutex::new(vec![]));
{
// 取得鎖並立刻呼叫 heavy_computation
let mut guard = shared.lock().unwrap();
heavy_computation(&mut *guard);
// 此時 guard 仍持有鎖,若在此期間有其他 thread 嘗試 lock,會被阻塞
} // guard 在此離開作用域,鎖被釋放
println!("結果: {:?}", *shared.lock().unwrap());
}
最佳實踐:將 鎖的持有時間縮到最小,只在真的需要讀寫資料時才 lock,避免長時間阻塞其他執行緒。
5. try_lock:非阻塞的嘗試
有時候我們不想讓執行緒因為等待鎖而卡住,Mutex::try_lock 會立即回傳 Result<MutexGuard<T>, TryLockError>,讓我們自行決定後續行為。
use std::sync::{Arc, Mutex, TryLockError};
use std::thread;
use std::time::Duration;
fn main() {
let data = Arc::new(Mutex::new(0));
// 先佔用鎖
let lock = data.lock().unwrap();
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
// 嘗試取得鎖,若失敗則直接返回
match data_clone.try_lock() {
Ok(mut guard) => {
*guard += 10;
println!("成功取得鎖,值變成 {}", *guard);
}
Err(TryLockError::WouldBlock) => {
println!("鎖被佔用,放棄本次操作");
}
Err(e) => panic!("其他錯誤: {:?}", e),
}
});
// 模擬長時間持有鎖
thread::sleep(Duration::from_millis(200));
drop(lock); // 釋放鎖
handle.join().unwrap();
}
使用情境:在 UI 主執行緒或實時系統中,避免阻塞 是關鍵,
try_lock能讓程式在鎖被佔用時採取備援策略。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 建議的解決方式 |
|---|---|---|
忘記使用 Arc,直接把 Mutex<T> 傳給多執行緒 |
編譯錯誤:Mutex<T> 不是 Send,或執行緒無法取得所有權 |
使用 Arc::new(Mutex::new(...)),每個執行緒 Arc::clone |
| 持有鎖的時間過長(例如在鎖內做 I/O) | 其他執行緒被長時間阻塞,導致效能瓶頸甚至死鎖 | 將 I/O 或耗時運算搬到鎖外,只在必要時 lock |
| 死鎖(兩個以上的 Mutex 交叉取得) | 程式永遠卡住,無法結束 | 固定鎖的取得順序,或使用 try_lock + 重試機制 |
| 鎖被 poison(panic 時未釋放) | 後續 .lock() 會回傳 PoisonError,若直接 unwrap 會 panic |
使用 `lock().unwrap_or_else( |
過度使用 Mutex,本可以用 Atomic* 完成 |
不必要的上下文切換,效能下降 | 若只需要單一原子操作,考慮 AtomicUsize、AtomicBool 等 |
在 Arc 內部直接使用 RefCell |
RefCell 只保證單執行緒內部可變,跨執行緒會 panic |
使用 Mutex/RwLock 包裝 RefCell,或改用 Mutex 直接 |
最佳實踐小結
- 最小化臨界區:只在需要讀寫時才 lock,盡量把計算、I/O、等待等操作放到鎖外。
- 處理 Poison:
Mutex在 panic 後會被 poison,使用unwrap_or_else或into_inner取得安全的內部值。 - 使用
Arc::clone而非Arc::new:避免不必要的資料拷貝。 - 考慮
RwLock:讀多寫少的情況下,RwLock能讓多個執行緒同時讀取,提高併發度。 - 測試與偵錯:使用
cargo test -- --test-threads=1測試併發程式,或加入loom這類模型檢查工具,提前發現資料競爭與死鎖。
實際應用場景
1. Web 伺服器的請求計數器
在高流量的 HTTP 伺服器中,我們常需要統計已處理的請求數。使用 Arc<Mutex<u64>> 可以在每個工作執行緒結束時安全遞增計數。
2. 並行資料處理(Map‑Reduce)
假設有一個大型檔案要切分成多塊,同時由多個執行緒解析,最終把結果寫入同一個 Vec<Result>。Arc<Mutex<Vec<_>>> 讓每個執行緒在完成子任務後 push 結果,而不必擔心資料競爭。
3. 共享快取(Cache)
在微服務或 CLI 工具中,某些計算結果可以緩存起來供多個執行緒重複使用。Arc<Mutex<HashMap<Key, Value>>> 能提供「寫入時加鎖、讀取時加鎖」的簡易快取實作。
4. 實時監控儀表板
若要在背景執行緒持續收集系統指標(CPU、記憶體),而主執行緒則負責渲染 UI,Arc<Mutex<Metrics>> 讓兩者安全共享最新的指標資料。
5. 多執行緒測試框架
測試框架往往需要在多個測試執行緒間共享測試狀態(例如測試失敗計數)。使用 Arc<Mutex<TestState>> 能確保測試結果正確累加,同時避免 race condition。
總結
Mutex為互斥鎖,保證同一時間只有一個執行緒能寫入被保護的資料。Arc為原子化的引用計數,讓多個執行緒能共享同一個Mutex(或其他Sync資源)。Arc<Mutex<T>>是 Rust 中最常見的共享可變狀態組合,使用時務必 最小化鎖持有時間、處理 Poison、避免死鎖。- 透過
try_lock、RwLock、Atomic*等工具,我們可以在不同需求下取得更好的效能與彈性。
掌握了這兩個同步原語,你就能在 Rust 中安全地構建高併發、可擴充的系統,從簡單的請求計數器到複雜的資料管線,都能以 「安全」+「效能」 為核心設計原則。祝你在 Rust 的併發世界裡玩得開心,寫出既快又穩的程式!