在前面的篇章中,我們理解了 Rust 如何在編譯期防止記憶體錯誤。
今天我們要面對一個現實:編譯器無法防止所有邏輯錯誤。
關鍵問題是:如何系統性地診斷和驗證我們的假設?
// ✅ 懸空指標:編譯器阻止
fn dangling_pointer() {
let r;
{
let x = 5;
r = &x; // ❌ 編譯錯誤
}
}
// ✅ 資料競爭:編譯器阻止
fn data_race() {
let mut x = 5;
let r1 = &x;
let r2 = &mut x; // ❌ 編譯錯誤
}
// ✅ 迭代器失效:編譯器阻止
fn iterator_invalidation() {
let mut vec = vec![1, 2, 3];
for item in &vec {
vec.push(4); // ❌ 編譯錯誤
}
}
// ❌ 記憶體洩漏:合法但不理想
fn memory_leak() {
let data = Box::new([0u8; 1024 * 1024]);
std::mem::forget(data); // ✅ 編譯通過,但記憶體洩漏
}
// ❌ 死鎖:合法但會卡住
use std::sync::Mutex;
fn deadlock() {
let lock1 = Mutex::new(0);
let lock2 = Mutex::new(0);
let _g1 = lock1.lock().unwrap();
let _g2 = lock2.lock().unwrap();
// 另一個執行緒以相反順序鎖定 -> 死鎖
}
// ❌ 邏輯錯誤:合法但結果錯誤
fn logic_error() {
let mut sum = 0;
for i in 0..10 {
sum += i; // 應該是 sum += i + 1?
}
}
重點:Rust 保證記憶體安全,但無法保證邏輯正確。我們需要工具來驗證邏輯。
Miri 是 Rust 的中間表示 (MIR) 解釋器,可以檢測:
# 安裝 Miri
rustup +nightly component add miri
# 執行測試
cargo +nightly miri test
# 執行特定測試
cargo +nightly miri test test_name
// 這段程式碼看起來沒問題
fn suspicious_code() {
let mut data = vec![1, 2, 3];
let ptr = data.as_mut_ptr();
unsafe {
// 越界存取
*ptr.add(10) = 42; // Miri 會檢測到
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_suspicious() {
suspicious_code();
// cargo +nightly miri test
// 會報告:memory access out of bounds
}
}
fn leak_detection() {
let data = Box::new(42);
std::mem::forget(data); // 記憶體洩漏
}
#[cfg(test)]
mod tests {
#[test]
fn test_leak() {
leak_detection();
// cargo +nightly miri test
// 會報告:memory leak detected
}
}
併發 bug 難以重現,因為執行緒排程是不確定的。Loom 透過窮舉所有可能的執行順序來檢測問題。
use loom::sync::Arc;
use loom::sync::atomic::{AtomicUsize, Ordering};
use loom::thread;
#[test]
fn test_concurrent_increment() {
loom::model(|| {
let counter = Arc::new(AtomicUsize::new(0));
let threads: Vec<_> = (0..2)
.map(|_| {
let counter = Arc::clone(&counter);
thread::spawn(move || {
counter.fetch_add(1, Ordering::SeqCst);
})
})
.collect();
for t in threads {
t.join().unwrap();
}
let final_count = counter.load(Ordering::SeqCst);
assert_eq!(final_count, 2);
});
}
use loom::sync::Mutex;
#[test]
#[should_panic]
fn test_deadlock_detection() {
loom::model(|| {
let lock1 = Arc::new(Mutex::new(0));
let lock2 = Arc::new(Mutex::new(0));
let lock1_clone = Arc::clone(&lock1);
let lock2_clone = Arc::clone(&lock2);
// 執行緒 1
let t1 = thread::spawn(move || {
let _g1 = lock1_clone.lock().unwrap();
let _g2 = lock2_clone.lock().unwrap();
});
// 執行緒 2:相反順序
let _g2 = lock2.lock().unwrap();
let _g1 = lock1.lock().unwrap();
t1.join().unwrap();
// Loom 會檢測到死鎖
});
}
# 啟用 AddressSanitizer
RUSTFLAGS="-Z sanitizer=address" cargo +nightly build --target x86_64-unknown-linux-gnu
# 執行測試
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu
// ASan 可以檢測的問題
fn asan_test() {
let mut vec = vec![1, 2, 3];
let ptr = vec.as_mut_ptr();
unsafe {
// 釋放後使用
drop(vec);
*ptr = 42; // ASan 會檢測到
}
}
# 啟用 ThreadSanitizer
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test --target x86_64-unknown-linux-gnu
use std::sync::Arc;
use std::thread;
// TSan 可以檢測資料競爭
fn tsan_test() {
let data = Arc::new(std::cell::Cell::new(0));
let data_clone = Arc::clone(&data);
let t = thread::spawn(move || {
data_clone.set(1); // 寫入
});
data.set(2); // 同時寫入 -> TSan 會檢測到
t.join().unwrap();
}
# 安裝
cargo install flamegraph
# 生成火焰圖
cargo flamegraph --bin my_program
# 會生成 flamegraph.svg
use criterion::{black_box, criterion_group, criterion_main, Criterion};
fn fibonacci(n: u64) -> u64 {
match n {
0 => 1,
1 => 1,
n => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);
# 使用 heaptrack (Linux)
heaptrack ./target/release/my_program
# 使用 Instruments (macOS)
instruments -t "Allocations" ./target/release/my_program
# 使用 valgrind (Linux)
valgrind --tool=massif ./target/release/my_program
use std::sync::Mutex;
// 糟糕:沒有固定的鎖順序
fn bad_locking(lock1: &Mutex<i32>, lock2: &Mutex<i32>) {
let _g1 = lock1.lock().unwrap();
let _g2 = lock2.lock().unwrap();
// 如果另一個執行緒以相反順序鎖定 -> 死鎖
}
// 好的:固定的鎖順序
fn good_locking(lock1: &Mutex<i32>, lock2: &Mutex<i32>) {
// 總是按照記憶體位址順序鎖定
let (first, second) = if lock1 as *const _ < lock2 as *const _ {
(lock1, lock2)
} else {
(lock2, lock1)
};
let _g1 = first.lock().unwrap();
let _g2 = second.lock().unwrap();
}
use std::sync::Mutex;
use std::time::Duration;
fn try_lock_pattern(lock1: &Mutex<i32>, lock2: &Mutex<i32>) -> Option<(i32, i32)> {
let g1 = lock1.lock().unwrap();
// 嘗試鎖定,如果失敗就放棄
match lock2.try_lock() {
Ok(g2) => Some((*g1, *g2)),
Err(_) => {
drop(g1); // 釋放第一個鎖
std::thread::sleep(Duration::from_millis(10));
None
}
}
}
use parking_lot::{Mutex, deadlock};
use std::thread;
use std::time::Duration;
// 啟用死鎖檢測
fn enable_deadlock_detection() {
thread::spawn(move || {
loop {
thread::sleep(Duration::from_secs(10));
let deadlocks = deadlock::check_deadlock();
if !deadlocks.is_empty() {
println!("檢測到 {} 個死鎖", deadlocks.len());
for (i, threads) in deadlocks.iter().enumerate() {
println!("死鎖 #{}", i);
for t in threads {
println!("執行緒 ID: {:?}", t.thread_id());
println!("堆疊追蹤:\n{:?}", t.backtrace());
}
}
}
}
});
}
use std::sync::atomic::{AtomicUsize, Ordering};
static ALLOCATION_COUNT: AtomicUsize = AtomicUsize::new(0);
struct Tracked<T> {
data: T,
}
impl<T> Tracked<T> {
fn new(data: T) -> Self {
ALLOCATION_COUNT.fetch_add(1, Ordering::SeqCst);
Tracked { data }
}
}
impl<T> Drop for Tracked<T> {
fn drop(&mut self) {
ALLOCATION_COUNT.fetch_sub(1, Ordering::SeqCst);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_leak() {
let initial = ALLOCATION_COUNT.load(Ordering::SeqCst);
{
let _data = Tracked::new(42);
}
let final_count = ALLOCATION_COUNT.load(Ordering::SeqCst);
assert_eq!(initial, final_count);
}
}
use std::rc::{Rc, Weak};
// 糟糕:循環引用導致洩漏
struct Node {
next: Option<Rc<Node>>,
}
fn circular_reference() {
let node1 = Rc::new(Node { next: None });
let node2 = Rc::new(Node { next: Some(Rc::clone(&node1)) });
// 如果 node1.next = Some(node2),就會洩漏
}
// 好的:使用 Weak 打破循環
struct BetterNode {
next: Option<Rc<BetterNode>>,
prev: Option<Weak<BetterNode>>, // 使用 Weak
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_functionality() {
let result = my_function(42);
assert_eq!(result, 84);
}
#[test]
fn test_edge_cases() {
assert_eq!(my_function(0), 0);
assert_eq!(my_function(i32::MAX), i32::MAX);
}
}
cargo +nightly miri test
# 使用 Loom
cargo test --features loom
# 使用 ThreadSanitizer
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test
# 生成火焰圖
cargo flamegraph --bin my_program
# 執行基準測試
cargo bench
# Linux
heaptrack ./target/release/my_program
# macOS
instruments -t "Leaks" ./target/release/my_program
// 借用檢查器是第一道防線
// 確保通過編譯
cargo +nightly miri test
# 檢測記憶體錯誤和洩漏
#[test]
fn test_concurrent() {
loom::model(|| {
// 測試所有可能的執行順序
});
}
# AddressSanitizer
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test
# ThreadSanitizer
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test
# 火焰圖
cargo flamegraph
# 基準測試
cargo bench
# 記憶體分析
heaptrack ./target/release/my_program
Rust 的編譯器只是第一道防線。
完整的品質保證需要多層次的診斷工具和系統性的測試策略。
在下一篇中,我們將探討 測試策略,看看如何建立完整的測試體系。