iT邦幫忙

2025 iThome 鐵人賽

DAY 28
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 28

(Day28) Rust 診斷與記憶體檢查:洩漏、競態、死鎖

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:診斷與記憶體檢查:洩漏、競態、死鎖

在前面的篇章中,我們理解了 Rust 如何在編譯期防止記憶體錯誤。

今天我們要面對一個現實:編譯器無法防止所有邏輯錯誤

關鍵問題是:如何系統性地診斷和驗證我們的假設?

Rust 能防止什麼,不能防止什麼?

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);  // ❌ 編譯錯誤
    }
}

Rust 無法防止的問題

// ❌ 記憶體洩漏:合法但不理想
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:解釋器級別的檢查

什麼是 Miri?

Miri 是 Rust 的中間表示 (MIR) 解釋器,可以檢測:

  • 未定義行為 (Undefined Behavior)
  • 記憶體洩漏
  • 資料競爭
  • 無效的記憶體存取

安裝和使用

# 安裝 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
    }
}

Miri 檢測記憶體洩漏

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
    }
}

Loom:併發測試框架

為什麼需要 Loom?

併發 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 會檢測到死鎖
    });
}

Sanitizers:執行期檢測工具

AddressSanitizer (ASan):記憶體錯誤檢測

# 啟用 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 (TSan):資料競爭檢測

# 啟用 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-flamegraph

# 安裝
cargo install flamegraph

# 生成火焰圖
cargo flamegraph --bin my_program

# 會生成 flamegraph.svg

使用 criterion 進行基準測試

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

死鎖檢測與預防

策略 1:鎖順序

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();
}

策略 2:try_lock

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
        }
    }
}

策略 3:使用 parking_lot

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());
                    }
                }
            }
        }
    });
}

記憶體洩漏檢測

策略 1:使用 Drop 追蹤

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);
    }
}

策略 2:使用 weak 引用

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
}

實戰案例:完整的診斷流程

1. 編寫測試

#[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);
    }
}

2. 執行 Miri 檢查

cargo +nightly miri test

3. 執行併發測試

# 使用 Loom
cargo test --features loom

# 使用 ThreadSanitizer
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test

4. 效能剖析

# 生成火焰圖
cargo flamegraph --bin my_program

# 執行基準測試
cargo bench

5. 記憶體分析

# Linux
heaptrack ./target/release/my_program

# macOS
instruments -t "Leaks" ./target/release/my_program

總結:診斷工具箱

1. 編譯期檢查

// 借用檢查器是第一道防線
// 確保通過編譯

2. Miri:未定義行為檢測

cargo +nightly miri test
# 檢測記憶體錯誤和洩漏

3. Loom:併發測試

#[test]
fn test_concurrent() {
    loom::model(|| {
        // 測試所有可能的執行順序
    });
}

4. Sanitizers:執行期檢測

# AddressSanitizer
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test

# ThreadSanitizer
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test

5. 效能工具

# 火焰圖
cargo flamegraph

# 基準測試
cargo bench

# 記憶體分析
heaptrack ./target/release/my_program

Rust 的編譯器只是第一道防線。
完整的品質保證需要多層次的診斷工具和系統性的測試策略。

在下一篇中,我們將探討 測試策略,看看如何建立完整的測試體系。

相關連結與參考資源


上一篇
(Day27) Rust unsafe 的最小暴露面:把風險關在最小區域
下一篇
(Day29) Rust 測試策略:單元、整合、性質測試
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言