本文 AI 產出,尚未審核

Rust 智能指標 — Arc<T>(原子參考計數)

簡介

在多執行緒(multithread)環境下,共享資料是常見需求。Rust 為了在保證安全性的前提下提供共享機制,設計了 Arc<T>(Atomic Reference Counted)型別。Arc<T> 能讓多個執行緒同時擁有同一筆資料的所有權,而不需要手動管理記憶體釋放或擔心資料競爭。

本篇文章將從概念、語法、實作範例一路說明,並探討常見的陷阱與最佳實踐,幫助從初學者到中級開發者在實務專案中安全、有效地使用 Arc<T>


核心概念

1. 為什麼需要 Arc<T>

  • Rc<T>(Reference Counted)只能在單執行緒使用,因為其內部計數器不是原子操作。
  • 當資料需要在多執行緒間共享時,必須使用 原子 參考計數,這就是 Arc<T> 的角色。

簡單來說Arc<T> = Rc<T> + 原子操作 = 多執行緒安全的共享指標。

2. Arc<T> 的基本使用方式

use std::sync::Arc;

let data = Arc::new(5);          // 建立一個 Arc 包住的 i32
let a = Arc::clone(&data);       // 增加引用計數,得到另一個 Arc
println!("{}", a);              // 印出 5
  • Arc::new 建立資料的唯一擁有者。
  • Arc::clone 不會 深拷貝資料,只是把引用計數加一。

3. Arc<T>Mutex<T>RwLock<T> 的配合

Arc<T> 本身只能保證 指標本身 的線程安全,資料內容 若需要可變,必須再包一層同步原語(如 Mutex<T>)。

use std::sync::{Arc, Mutex};

let shared_vec = Arc::new(Mutex::new(vec![1, 2, 3]));
let mut handles = vec![];

for i in 0..3 {
    let vec_clone = Arc::clone(&shared_vec);
    handles.push(std::thread::spawn(move || {
        let mut guard = vec_clone.lock().unwrap(); // 取得鎖
        guard.push(i);
    }));
}
for h in handles { h.join().unwrap(); }

println!("{:?}", *shared_vec.lock().unwrap()); // [1,2,3,0,1,2]

4. Arc<T>Weak<T>

Weak<T>Arc<T>弱引用,不會增加引用計數,常用於避免循環引用(reference cycle)。

use std::sync::{Arc, Weak};

let strong = Arc::new(10);
let weak: Weak<i32> = Arc::downgrade(&strong); // 產生弱引用

assert_eq!(weak.upgrade().as_ref(), Some(&strong)); // 仍可升級
drop(strong); // 強引用被釋放
assert!(weak.upgrade().is_none()); // 升級失敗,表示資料已被釋放

5. Arc<T> 的內部實作(簡述)

  • Arc<T> 包含一個 原子整數AtomicUsize)作為引用計數。
  • 每次 clone 時執行 fetch_add(1, Ordering::Relaxed)
  • 每次 drop 時執行 fetch_sub(1, Ordering::Release),若結果為 0,則呼叫 dealloc 釋放底層資料。

注意:雖然 Arc<T> 本身是線程安全的,但 T 的內部是否安全仍取決於 T 本身的特性(SendSync)。


程式碼範例

範例 1:最簡單的 Arc 共享

use std::sync::Arc;
use std::thread;

let number = Arc::new(42);
let mut handles = vec![];

for _ in 0..4 {
    let n = Arc::clone(&number);
    handles.push(thread::spawn(move || {
        println!("執行緒看到的值: {}", n);
    }));
}
for h in handles { h.join().unwrap(); }

說明:四個執行緒同時讀取同一筆資料,不需要任何鎖。

範例 2:Arc + Mutex 實作安全的寫入

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        let mut num = c.lock().unwrap(); // 取得互斥鎖
        *num += 1;
    }));
}
for h in handles { h.join().unwrap(); }

println!("最終計數: {}", *counter.lock().unwrap()); // 10

說明Mutex 確保同時只有一個執行緒可以修改計數器。

範例 3:使用 Arc + RwLock 讀寫分離

use std::sync::{Arc, RwLock};
use std::thread;

let data = Arc::new(RwLock::new(vec![1, 2, 3]));

let writer = {
    let d = Arc::clone(&data);
    thread::spawn(move || {
        let mut w = d.write().unwrap(); // 寫入鎖
        w.push(4);
    })
};

let reader = {
    let d = Arc::clone(&data);
    thread::spawn(move || {
        let r = d.read().unwrap(); // 讀取鎖
        println!("讀取結果: {:?}", *r);
    })
};

writer.join().unwrap();
reader.join().unwrap();

說明RwLock 允許多個讀者同時存取,寫者則獨占。

範例 4:避免循環引用的 Weak 使用

use std::sync::{Arc, Weak, Mutex};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: Mutex<Weak<Node>>, // 弱引用指向父節點
    children: Mutex<Vec<Arc<Node>>>,
}

let root = Arc::new(Node {
    value: 0,
    parent: Mutex::new(Weak::new()),
    children: Mutex::new(vec![]),
});

let child = Arc::new(Node {
    value: 1,
    parent: Mutex::new(Arc::downgrade(&root)),
    children: Mutex::new(vec![]),
});

root.children.lock().unwrap().push(Arc::clone(&child));

// 循環引用已被斷開,當 root、child 都 drop 時不會泄漏

說明:父節點使用 Weak,避免 Arc 之間形成不可釋放的循環。

範例 5:ArcAtomicUsize 結合做簡易計數器

use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
use std::thread;

let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];

for _ in 0..5 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        for _ in 0..1000 {
            c.fetch_add(1, Ordering::Relaxed);
        }
    }));
}
for h in handles { h.join().unwrap(); }

println!("計數結果: {}", counter.load(Ordering::Relaxed)); // 5000

說明:若只需要原子計數,直接使用 AtomicUsize,不必再包 Mutex


常見陷阱與最佳實踐

陷阱 可能的後果 解決方式
誤以為 Arc<T> 本身提供可變性 多執行緒同時寫入會產生資料競爭,甚至程式 panic 必須搭配 Mutex<T>RwLock<T> 或其他同步原語
忘記 Arc::clone,直接傳遞 Arc<T> 會移動所有權,導致後續無法再使用 使用 Arc::clone(&arc) 產生新引用
形成循環引用 (ArcArc) 記憶體永遠不會被釋放,造成 leak 使用 Weak<T> 作為回指(如樹狀結構)
過度使用 Arc 每次 clone 都會執行原子操作,若頻繁且不跨執行緒,效能不佳 在單執行緒內使用 Rc<T>,或直接傳遞裸指標/引用
忘記 Ordering 的正確選擇 在極端情況下可能出現不可預期的行為 大多數情況使用 Relaxed(計數)或 SeqCst(同步)即可,除非有特殊需求

最佳實踐

  1. 只在跨執行緒時使用 Arc,單執行緒環境優先考慮 Rc
  2. 將可變資料封裝在 Mutex / RwLock,避免直接在 Arc<T> 上做可變操作。
  3. 盡量將 Arc 的生命週期限制在最小範圍,使用 let 區塊或函式參數傳遞,減少不必要的引用計數。
  4. 檢查是否真的需要共享,有時候傳遞 &T&mut T 更簡潔且效能更好。
  5. 使用 Weak 斷開可能的循環,特別是在樹、圖、或 actor 系統等結構中。

實際應用場景

  1. Web 伺服器的全域設定
    多個 worker 執行緒需要讀取相同的設定檔或資料庫連線池。將設定包在 Arc<Config> 中,所有執行緒共享同一份只讀資料。

  2. 多執行緒的緩存(Cache)
    使用 Arc<Mutex<HashMap<K, V>>>,讓多個執行緒可以同時讀寫快取,且在快取被清除時自動釋放記憶體。

  3. Actor 系統或訊息傳遞框架
    每個 actor 持有 Arc<Context>,而 Context 內部可能包含 Weak 指向自身,以防止循環引用。

  4. 圖形或遊戲引擎的資源管理
    紋理、模型等資源以 Arc<Resource> 形式載入,保證在多個渲染執行緒間共享,同時在最後一個引用離開時自動釋放。

  5. 測試框架的共享狀態
    測試用例可能在多個執行緒中同時檢查同一個狀態變數,Arc<AtomicUsize>Arc<Mutex<T>> 能提供簡潔的同步機制。


總結

Arc<T> 是 Rust 在 多執行緒環境 中提供安全共享的核心工具。它透過原子參考計數讓多個執行緒可以同時擁有資料的所有權,同時配合 Mutex<T>RwLock<T>Atomic* 等同步原語,能滿足絕大多數的共享需求。使用時要留意:

  • 只在跨執行緒時使用,單執行緒環境可改用 Rc<T>
  • 資料可變必須加鎖,或使用原子類型。
  • 避免循環引用,必要時使用 Weak<T>
  • 適度評估效能,避免過度使用 Arc 帶來的原子操作開銷。

掌握這些概念後,你就能在實務專案中以 簡潔且安全 的方式管理跨執行緒的共享資源,寫出更具可維護性與效能的 Rust 程式碼。祝你玩得開心,寫得順利!