在 Rust 中,智慧指標是為了解決一個主要問題而存在的:
如何在沒有垃圾回收器的情況下,安全、高效地管理記憶體,尤其是在複雜的所有權和併發場景下。
我們從最簡單的需求開始,一步步 tackle 真正棘手的問題,來明白為什麼 Box
、Rc
、Arc
以及它們的搭檔 RefCell
和 Mutex
是這樣設計的。
預設情況下,資料放在堆疊(stack)上。
但有時候你必須把它放到堆(heap)上,原因很簡單:
體積太大:你不想在函式呼叫時複製一個巨大的物件。
大小不確定:編譯器不知道這東西有多大(比如遞迴類型)。
需要轉移所有權:你想明確地說:「這塊記憶體的管理權現在交給你了」。
Box<T>
— 唯一的堆記憶體所有者Box<T>
是你能用的最簡單、最基礎的工具。
它的工作只有一件:在堆上申請一塊記憶體,然後成為這塊記憶體唯一的主人。
struct SomeData {
value: i32,
}
fn main() {
// some_data 被配置在堆上。
// boxed_data 是指向它的唯一指標,擁有其所有權。
let boxed_data = Box::new(SomeData { value: 42 });
// 直接使用,沒有廢話。
println!("Value is: {}", boxed_data.value);
} // 當 boxed_data 離開作用域,它會自動被銷毀。
// Rust 會立即釋放堆上的記憶體。乾淨、俐落。
結論:當你需要將資料放在堆上,並且永遠只會有「一個」所有者時,用 Box<T>
。別想太多,這就是它的用途。
堆疊 (Stack):
我們在程式碼中宣告的變數
boxed_data
實際上位於堆疊上。但它本身不是
SomeData
這個結構,而是一個智慧指標。它的體積很小且固定,裡面存放著一個記憶體地址。堆 (Heap):
SomeData { value: 42 }
這個真正的、可能很龐大的資料,被配置在堆記憶體上。這就是你為它> 找到的「穩定的家」。擁有並指向 (Owns and Points to):
堆疊上的
boxed_data
指標指向堆上的SomeData
。
Box<T>
最重要的特性是所有權:boxed_data
是這塊堆記憶體的唯一擁有者。當
boxed_data
離開作用域時 (例如函式結束),它會自動觸發drop
,並釋放它在堆上所擁有> > 的資料。這個過程確保了沒有記憶體洩漏,而且乾淨俐落。
想像一下,多個地方都需要讀取一份共享設定。複製 N 份是愚蠢的行為,浪費記憶體。你需要讓多個變數指向「同一份」資料。
用 Box
是行不通的,因為它的所有權是唯一的。
Rc<T>
— 非原子性的參考計數Rc<T>
(Reference Counted) 的核心思想很直接:記錄有多少個指標正指向同一塊資料。
Rc::new(data)
:創建一個 Rc,參考計數為 1。
Rc::clone(&rc_data)
:這不是深度複製! 這只是增加參考計數,然後給你一個指向相同資料的新指標。這個操作非常、非常快。
當一個 Rc
指標離開作用域,計數減 1。
當計數歸零,資料被自動銷毀。
use std::rc::Rc;
fn main() {
// 初始計數為 1
let shared_data = Rc::new(String::from("shared config"));
println!("Initial count: {}", Rc::strong_count(&shared_data));
// reference1 共享所有權
let reference1 = Rc::clone(&shared_data);
println!("Count after first clone: {}", Rc::strong_count(&shared_data));
{
// reference2 也共享所有權
let reference2 = Rc::clone(&shared_data);
println!("Count after second clone: {}", Rc::strong_count(&shared_data));
} // reference2 離開作用域,計數減 1
println!("Count after reference2 goes out of scope: {}", Rc::strong_count(&shared_data));
}
重要限制:Rc
的計數器不是「原子」的,它在多執行緒環境下會出錯。
這是故意設計的,目的是讓你不必為你用不到的功能(執行緒安全)支付效能代價。
共享所有權: 在堆疊上的三個不同變數 (
shared_data
,reference1
,reference2
) 都指向堆上的同一塊記憶體。參考計數:
Rc<T>
的核心機制。在堆上的資料旁,有一個計數器。每當Rc::clone()
被呼叫,計數器就+1
。當任何一個指標離開作用域時,計數器就-1
。自動清理: 當參考計數變為
0
時,代表已經沒有任何指標指向這份資料,Rust 會自動釋放這塊堆記憶體。
如果你試圖把 Rc
丟進另一個執行緒,編譯器會直接拒絕你。這是好事,它阻止你犯下愚蠢的錯誤。
Arc<T>
— 原子性的參考計數Arc<T>
(Atomically Reference Counted) 就是 Rc<T>
的多執行緒版本。
API 幾乎一樣,唯一的、也是最重要的區別是:Arc
使用原子操作來增減參考計數。
原子操作確保了即使多個執行緒同時 clone
或 drop
,計數器也不會出錯。這是執行緒安全的保證,當然也會帶來一點點效能開銷。你為安全付費。
use std::sync::Arc;
use std::thread;
fn main() {
let shared_data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let data_clone = Arc::clone(&shared_data);
let handle = thread::spawn(move || {
println!("Thread {}: sees data {:?}", i, data_clone);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
結論:Rc
用於單執行緒,Arc
用於多執行緒。它們解決了唯讀資料的共享問題。
跨執行緒共享: 圖中的
Main Thread
、Thread 1
和Thread 2
代表不同的執行緒。Arc<T>
允許這三個執行緒安全地共同擁有並存取堆上的同一份資料。原子性計數: 與
Rc
的主要區別在於,Arc
的參考計數是原子性的。這表示即使多個執行緒同時嘗試增加或減少計數,計數器也能保證不會出錯,避免了資料競爭。這是它執行緒安全的關鍵。
上面所有的例子都有一個共同點:共享的資料是不可變的。
現實世界沒這麼美好,我們經常需要在共享的同時修改資料。
如果你直接嘗試修改 Arc<T>
裡面的東西,編譯器會再次阻止你。
為什麼?
因為如果多個執行緒能同時修改同一塊資料,就會產生「資料競爭」(Data Race),這是萬惡之源。
為了解決這個問題,需要引入「內部可變性」(Interior Mutability)的概念,並使用同步工具。
Rc<RefCell<T>>
在單執行緒中,我們不需要 Mutex
那樣的重型武器,但仍然需要一種機制來確保在任何時候,要嘛只有一個可變引用,要嘛有多個不可變引用。RefCell<T>
把編譯器的靜態借用檢查,變成了執行期的動態檢查。
borrow()
:取得一個不可變引用。如果已經存在可變引用,則在執行期 panic
。borrow_mut()
:取得一個可變引用。如果已經存在任何其他引用(可變或不可變),則在執行期 panic
。use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// 使用 RefCell 包裹我們想要修改的資料
let shared_data = Rc::new(RefCell::new(vec![1, 2, 3]));
let data1 = Rc::clone(&shared_data);
let data2 = Rc::clone(&shared_data);
// 我們可以透過 RefCell 修改資料
data1.borrow_mut().push(4);
// 我們也可以讀取資料
println!("Data after modification: {:?}", data2.borrow());
}
Arc<Mutex<T>>
這是在併發程式設計中最常見的模式之一。Mutex<T>
(Mutual Exclusion) 就像一把鎖。
Mutex 是一個鎖:它確保在任何時間點,只有一個執行緒能存取被它保護的資料。
lock()
方法:當一個執行緒呼叫 lock()
,它會請求鑰匙。如果鑰匙可用,它就拿到鑰匙並鎖上門,開始操作資料。如果鑰匙被別的執行緒拿走了,它就必須等待,直到鑰匙被歸還。
鎖衛士 (Lock Guard):lock()
會回傳一個「鎖衛士」。當這個衛士離開作用域時,鎖會被自動釋放。這極大地避免了忘記解鎖導致的死鎖問題。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Mutex 保護著我們的計數器
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
// 請求鎖,如果被佔用就等待
// lock() 回傳一個 Result,我們 unwrap 它
let mut num = counter_clone.lock().unwrap();
*num += 1;
}); // 當 `num` (鎖衛士) 離開作用域,鎖會自動釋放
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // 最終結果是 10
}
註:RwLock<T>
是 Mutex<T>
的一個變種,它允許「多個讀者」或「一個寫者」,適用於讀多寫少的場景。
結構:
Arc
負責讓不同執行緒都能「看到」同一個Mutex
。Mutex
則像一個保險箱,把真正的資料0
保護在裡面。互斥 (Mutual Exclusion):
執行緒 1 呼叫了
lock()
並成功取得了鎖。它得到一個MutexGuard
(鎖衛士,圖中的num
),只有透過這個衛士才能存取並修改資料。此時,執行緒 2 也想呼叫
lock()
,但因為鎖已經被執行緒 1 持有,它就必須等待 (Blocked),直到執行緒 1 結束操作。自動解鎖: 當
num
(鎖衛士) 在執行緒 1 中離開作用域時,鎖會自動釋放,這時執行緒 2 才能獲得鎖並繼續執行。這確保了任何時候都只有一個執行緒能修改資料。
可以按照這個思路來思考:
這才是完整的 picture。
Rust 提供了不同層級的工具,每種工具都有其特定的使用場景和效能開銷。