本文 AI 產出,尚未審核

Rust 並行與多執行緒:共享狀態(MutexArc

簡介

在多執行緒程式中,共享狀態是最常見也最容易出錯的部分。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> 實作了 DerefDerefMut,因此我們可以像操作 &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* 完成 不必要的上下文切換,效能下降 若只需要單一原子操作,考慮 AtomicUsizeAtomicBool
Arc 內部直接使用 RefCell RefCell 只保證單執行緒內部可變,跨執行緒會 panic 使用 Mutex/RwLock 包裝 RefCell,或改用 Mutex 直接

最佳實踐小結

  1. 最小化臨界區:只在需要讀寫時才 lock,盡量把計算、I/O、等待等操作放到鎖外。
  2. 處理 PoisonMutex 在 panic 後會被 poison,使用 unwrap_or_elseinto_inner 取得安全的內部值。
  3. 使用 Arc::clone 而非 Arc::new:避免不必要的資料拷貝。
  4. 考慮 RwLock:讀多寫少的情況下,RwLock 能讓多個執行緒同時讀取,提高併發度。
  5. 測試與偵錯:使用 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_lockRwLockAtomic* 等工具,我們可以在不同需求下取得更好的效能與彈性。

掌握了這兩個同步原語,你就能在 Rust 中安全地構建高併發、可擴充的系統,從簡單的請求計數器到複雜的資料管線,都能以 「安全」+「效能」 為核心設計原則。祝你在 Rust 的併發世界裡玩得開心,寫出既快又穩的程式!