[Rust] Box vs. Arc 完整指南:所有權模型、性能比較與多執行緒共享
Rust 新生第一道送命題:Arc 還是 Box?
別急著翻官方文件,先把三個靈魂拷問擺在桌上:
- 誰會摸這塊資料? 只有你自己,還是一群飢渴執行緒?
- 它常常要被改嗎? 讀寫頻率決定你願不願意為鎖或原子計數付出開銷
- 快 vs. 安心:你更怕延遲還是怕資料競爭?
如果以上問題讓你腦袋卡死,那恭喜,這篇文章就是為此存在。我不會直接把「標準答案」硬塞進你腦袋,而是陪你拆零件、跑 benchmark、畫記憶體佈局,最後讓你自己踩線分勝負
Box : Rust 新生必修的 Heap 小魔法
「我只想塞一個 10 MB 的陣列,為什麼編譯器吵著 stack overflow?」
如果你有過這種困惑,那八成就是 Box 要登場的時候了
Box 是什麼?
- 把任何
T
搬到 heap,並在 stack 留下一支 8 bytes(在 64‑bit 平台)的指標 - 想像你租了一個倉庫(heap),把笨重家具(資料)拖進去,手上只留鑰匙(指標)。房租(配置成本)不免費,但空間大、樓板不會塌
什麼時候該用 Box ?
根據 Rust Book 的建議:
- 資料超大或大小編譯期算不出來:避免把巨無霸塞爆 stack
- 遞迴資料結構:tree、linked list 的節點無法在編譯期確定大小,用 Box 打破「無窮遞迴」僵局
- 大量資料轉移所有權:避免 stack 複製
- Trait Object:需要動態分派時
什麼時候不該用 Box ?
對於小物件、短生命週期 :
- 使用 Stack 分配更快,無 heap 分配開銷
- 沒有額外的記憶體管理成本
- Stack 非常快,是 Rust 預設的記憶體分配位置
- 經驗表明,將 allocation rate 降低 10 allocations per million instructions 就能帶來可測量的性能提升
我真的很菜,能看個例子嗎?
// ❌ 小物件不需要 Box
let x = Box::new(5); // 不必要的 heap 分配
// ✅ 大型資料使用 Box
struct LargeData {
data: [u8; 1_000_000]
}
let large = Box::new(LargeData { data: [0; 1_000_000] });
// ✅ 遞迴結構必須用 Box
enum List {
Cons(i32, Box<List>), // 沒有 Box 會編譯錯誤
Nil,
}
常見陷阱
- 以為 Box = 共用指標 → 不,它只有 單一所有者,別跨執行緒複製指標(編譯器也不讓你過)
- 掉進雙重 Box →
Box<Box<T>>
99% 情況下是 bad smell,要的多半是別的東西(也許Vec<T>
、也許Arc<T>
) - 忘記大小寫 →
Box
是 唯一 使用 PascalCase 命名的原生智慧指標,別手滑打成box
(那是保留關鍵字)
Box 快速小結
Box = 單執行緒、單 owner、堆放大物的保險箱
如果你的程式碼同時滿足以上三斷言,就放心把資料塞進 Box──乾淨、快速、零借用煩惱
Arc:多執行緒共享的團體票
「這塊資料不只我一條執行緒要用,怎麼辦?」
Arc 的答案:原子引用計數——誰拿走就+1,誰還回就-1,計數歸零才真正釋放
Arc 是什麼?
- Atomically Reference Counted,原子引用計數
- 替任意
T
發一張「多人共乘卡」,透過AtomicUsize
追蹤票數,確保釋放時機萬無一失 - 一桶共享飲料(資料)放在冰箱(heap)。每來一個室友(執行緒)就貼一張姓名貼(
Arc::clone
),只要牆上的貼紙 > 0,飲料就不會被倒掉 - Thread Safe:透過原子操作,確保多個執行緒能夠安全地創建和釋放
Arc
實例,而不會引起資料競爭或未定義的行為
什麼時候該用 Arc?
- 跨執行緒共讀:靜態設定、字典、只讀快取等
- 任務池 / worker threads:把同一份資料傳給多個
thread::spawn
、tokio::spawn
- 需要可變又想共享:配合
Mutex
/RwLock
(Arc<Mutex<T>>
) 實現 可變 + 多執行緒 雙保險
不該用 Arc 的時機 ?
- 單執行緒:
Rc
(甚至Box
)更輕盈,少掉原子指令的成本。 - 高頻 clone / drop 熱點:在極端高併發計數場景,原子爭用可能成為瓶頸;可考慮 thread‑local refcount 方案(如
triomphe::Arc
)
簡單例子
use std::sync::{Arc, Mutex};
use std::thread;
let numbers = Arc::new(Mutex::new(vec![0; 1_000]));
let mut handles = vec![];
for _ in 0..4 {
let nums = Arc::clone(&numbers); // +1
handles.push(thread::spawn(move || {
let mut data = nums.lock().unwrap();
data[0] += 1; // 安全可變,因為有 Mutex
}));
}
for h in handles { h.join().unwrap(); } // 四條執行緒跑完,引用計數歸零,自動釋放
常見陷阱
- Arc ≠ 資料自動 thread safe → Arc 只確保 指標 安全共享,要改值要上鎖
- 過度 clone → 每次
Arc::clone
都是原子加法,把Arc
傳到函式裡直接移動(fn foo(x: Arc<T>)
) 可省掉一次 - Reference cycle → 互指的
Arc
形成強循環;請搭配Weak
解環
Thread Safety in Arc(指標安全 ≠ 資料安全)
- Arc 保護的是「指標」
Arc::clone
與drop
都用 原子加減,再怎麼多執行緒同時操作也不會衝突 - Arc 不會自動上鎖
如果內容T
需要「多人同時改」,請包Arc<Mutex<T>>
或Arc<RwLock<T>>
,不包只能共讀,硬改會被編譯器擋下 - 原子操作的成本
- 每次
clone
/drop
多 2‒3 ns 的原子開銷 - 高併發下
Arc
的計數欄位會產生 cache‑line contention,如果成為 hot spot,考慮 triomphe::Arc 或其他 thread‑local refcount 技術
- 每次
Arc<T>
什麼時候是Send
?
只要T: Sync + Send
,整個Arc<T>
既可移動 (Send
) 又可共用 (Sync
) -> 否則編譯器會報錯提醒你「別跨執行緒」或「包一層鎖」
Arc vs. Rc:怎麼挑?
Arc | Rc | |
---|---|---|
執行緒安全 | ✔ (Send + Sync ) | ✘(單執行緒限定) |
計數類型 | AtomicUsize | usize |
clone / drop 成本 | 微幅增加(原子指令) | 最低(非原子) |
典型用途 | 多執行緒共讀,Arc<Mutex<T>> 共寫 | 單執行緒多所有者、圖/樹結構 |
只在「確定永遠單執行緒」且「計數操作很熱」的情境下選 Rc,其餘直接用 Arc 省重構
延伸閱讀: Rc 与 Arc 实现 1vN 所有权机制
選用建議
- 確定單執行緒 & 對效能斤斤計較 → 選 Rc,少掉原子開銷
- 可能日後上多執行緒、或嫌判斷麻煩 → 直接用 Arc,未雨綢繆成本低
- 要可變共享 → 包
Mutex
/RwLock
,兩者同理
Arc 快速小結
Arc = 多執行緒、共讀資料、安全退場的團體票
若你的資料有「大家都要看、偶爾要改」這兩需求,把它塞進 Arc<Mutex<T>>
幾乎是 Rust 社群的通用配方
大型資料 : Box 還是 Arc,還是……都要 ?
手上握著一坨 200 MB 的 JSON,要餵 4 條執行緒統計分析。該怎麼包?
- 只有一條執行緒
- 讀多寫少 →
Box<T>
,最省事、最省錢 - 寫也不少 → 仍用
Box
,照常可變,不用把自己嚇到上Arc
- 讀多寫少 →
- 至少兩條執行緒
- 純讀 →
Arc<T>
,原子引用計數的成本,和磁碟 IO 相比幾乎是噪音 - 需要改 →
Arc<Mutex<T>>
或Arc<RwLock<T>>
,鎖比資料毀損便宜
- 純讀 →
- 主執行緒吃最多,其餘偶爾查 →
Arc<Box<T>>
:- 大塊資料一次
Box
進 heap - 偶爾分享時再
Arc::clone
指標,少量原子操作換到彈性
- 大塊資料一次
// 範例:大型 read-only 字典,hotspot 在 main thread
let big_map = Box::new(build_big_hashmap()?); // heap 一次
let shared = Arc::new(big_map); // 0‑cost 轉 Arc
for _ in 0..4 {
let reader = Arc::clone(&shared); // +1 (atomic)
std::thread::spawn(move || {
// 唯讀 → 不用鎖
println!("{}", reader.get("answer").unwrap());
});
}
你真正關心的三件事
指標 | Box | Arc | Rc |
初始化成本 | 1× heap alloc | 1× heap + 1× atomic | 同 Arc |
clone 成本 | move = free | 原子加法 ≈ +2 ns | 同 Arc |
快取友善度 | ✔ 單層解引用 | ✘ 計數器 + cache‑line contention | ✔/✘ 視讀寫比例 |
處理極大型的資料結構時,需要仔細考慮記憶體碎片化和配置模式:
- Box 的優勢:配置通常更符合快取友好性,且更容易被配置器優化
- Arc 的挑戰:可能因為資料和引用計數是分別配置的,導致更多的記憶體碎片
決策關鍵不是資料大小,而是 所有權 與 執行緒,先畫清楚誰會動資料,再挑對智慧指標,少走回頭路
常見地雷
Box
裡再包Vec<u8>
? 可能重複配置。先問自己:真的需要雙重 indirection 嗎?- 高頻
Arc::clone
/drop
→ 計數器成為熱點,把指標 移交 給 closure、或用 triomphe::Arc 的 thread‑local refcount - 「鎖一定慢」的迷思 → 鎖的常數成本遠小於資料損壞的代價,別為了幾十奈秒去當記憶體炸彈客
結語
你現在總算翻到卷尾,應該對 Box
、Arc
這兩位「智慧指標界的大小姐」有了親身相處的心得。此刻的我不打算再幫你划重點,課本背完不代表你真的會開車,真正的功夫得在專案裡摔個幾跤,才知道哪些理論能撐住、哪些幻想會碎掉
所有權模型的美在於「逼你思考關係」。誰該擁有?誰能共用?誰必須讓步?那其實跟協作開發、團隊權責分配、甚至人生抉擇一樣──沒有完美答案,只有更清楚的取捨
在下一行 clone()
或 Box::new()
之前,先想清楚背後的故事,再優雅地下指令 (或按tab)
Comments ()