Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

poisoning

本集目標

理解什麼是鎖的中毒(poisoning),以及該怎麼處理它。

概念說明

.lock() 為什麼回傳 Result

前面幾集學 MutexRwLock 的時候,我們都寫 .lock().expect("鎖失敗")。但什麼時候取鎖會「失敗」?答案就是 poisoning

什麼是 poisoning

如果一個執行緒在持有 Mutex 鎖或 RwLock 寫鎖的期間 panic 了,鎖會被標記為「中毒」(poisoned)。之後任何執行緒再嘗試取鎖,Mutex::lockRwLockreadwrite 都會收到 Err(PoisonError)

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

fn main() {
    let data = Arc::new(Mutex::new(vec![1, 2, 3]));
    let data2 = Arc::clone(&data);

    let handle = thread::spawn(move || {
        let mut guard = data2.lock().expect("鎖失敗");
        guard.push(4);
        panic!("哎呀!"); // panic 的時候 guard 還活著 → 鎖中毒
    });

    let _ = handle.join(); // 收集 panic,不讓它傳播

    // 之後再 lock → Err
    match data.lock() {
        Ok(guard) => println!("正常:{:?}", *guard),
        Err(_poisoned) => println!("鎖中毒了!"),
    }
}

為什麼要有 poisoning

panic 通常代表程式出了預期外的錯誤。如果一個執行緒在修改資料修到一半就 panic 了,資料可能是半成品——比如 Vec push 了一半、或某兩個欄位只更新了一個。poisoning 是一個安全機制:讓你知道出事了,由你決定要不要繼續用。

三種處理方式

1. 直接 panic(最常見)

use std::sync::Mutex;

fn main() {
    let data = Mutex::new(Vec::<i32>::new());
    let guard = data.lock().expect("鎖失敗");
}

如果鎖中毒了,你的執行緒也跟著 panic。大部分情況這樣就好——上一個執行緒 panic 了,通常代表整個程式該結束了。

2. 忽略中毒,繼續用

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

fn main() {
    let data = Mutex::new(Vec::<i32>::new());
    let guard = data.lock().unwrap_or_else(PoisonError::into_inner);
}

PoisonError::into_inner 讓你拿回 guard,跳過中毒的警告。如果你確定資料的狀態沒問題,或者你不在意,可以這樣做。

要注意這是一個「每次取鎖都這樣寫」的做法:鎖本身從頭到尾都還是中毒狀態,你只是每一次都不理它。只要程式裡有一個地方忘了這樣寫、直接用了 lock().expect(...),那裡就會 panic。

3. 修復資料再繼續

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

fn main() {
    let data = Mutex::new(Vec::<i32>::new());
    let guard = match data.lock() {
        Ok(g) => g,
        Err(poisoned) => {
            let mut g = poisoned.into_inner();
            *g = vec![];         // 重設成已知的安全狀態
            data.clear_poison(); // 解除中毒,之後的 lock 才會恢復正常
            g
        }
    };
}

拿到 guard 之後把資料修復成合理的值,然後繼續用。

第 2 種做法可以一直不理中毒,是因為它每次取鎖都寫成同一個形式。但第 3 種做法的用意不一樣——它想把資料修好、然後回到正常運作。這時候只修資料是不夠的,程式裡其他地方老老實實寫的 lock() 還是會拿到 Err;修完之後要再呼叫 clear_poison(),才算真的解除中毒(RwLock 也有同名的方法)。

為什麼 .into_inner() 是安全的

你可能會好奇:中毒的鎖裡面的資料可能是半成品,存取它真的沒問題嗎?

從記憶體的角度來看是沒問題的。不管鎖有沒有中毒,裡面的資料都是合法的記憶體——不會存取到已經不能使用的記憶體、不會把型別搞混、沒有資料競爭。poisoning 保護的是邏輯一致性,不是記憶體安全。資料可能邏輯上不對,但從記憶體的角度看完全合法。所以可以安全地呼叫 .into_inner()

RwLock 的 poisoning

RwLock 的 poisoning 只在寫鎖 panic 的時候觸發。讀鎖 panic 不會中毒——因為讀的時候不會修改資料,不會留下不一致的狀態。但一旦中毒了,readwrite 都會回傳 Err

範例程式碼

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

fn main() {
    let counter = Arc::new(Mutex::new(0));

    // 啟動一個會 panic 的執行緒
    let counter2 = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        let mut guard = counter2.lock().expect("鎖失敗");
        *guard += 1;
        panic!("糟糕,出事了!");
    });

    // 等待那個執行緒結束(那個執行緒會 panic,但我們用 let _ 忽略)
    let _ = handle.join();

    // 嘗試取鎖——會收到 PoisonError
    match counter.lock() {
        Ok(guard) => {
            println!("正常取得鎖,值 = {}", *guard);
        }
        Err(poisoned) => {
            println!("鎖中毒了!");

            // 拿到資料看看
            let guard = poisoned.into_inner();
            println!("裡面的值 = {}", *guard);
        }
    }

    // 或者用一行忽略中毒
    let guard = counter.lock().unwrap_or_else(PoisonError::into_inner);
    println!("忽略中毒,值 = {}", *guard);
}

重點整理

  • 執行緒在持有 Mutex 鎖或 RwLock 寫鎖期間 panic → 鎖中毒(poisoned)。
  • 之後 lock / read / write 都回傳 Err(PoisonError)
  • RwLock 只有寫鎖 panic 才會中毒,讀鎖 panic 不會。
  • PoisonError::into_inner 可以拿回 guard——記憶體安全沒問題,只是邏輯一致性的問題。
  • 三種處理方式:
    • panic(.unwrap().expect())。
    • 忽略(.unwrap_or_else(PoisonError::into_inner))。
    • 修復資料再繼續(into_inner 不會解除中毒,修好之後要 clear_poison() 才算真的復原)。