偵錯鎖定依附元件週期

鎖定依附元件週期是造成死結的常見原因。本指南提供相關操作說明,協助您偵測、偵錯及解決鎖定依附元件週期。

荒漠油廠

Fuchsia 上的 Rust 程式可以使用 fuchsia_sync 鎖定,從額外的執行階段檢查中獲益,偵測可能造成死結的存取模式。

這些檢查會依賴 tracing_mutex Crate,偵測不同執行緒間的鎖定取得作業週期。

採用 fuchsia_sync

如要在程式碼中使用 fuchsia_sync,請按照下列步驟操作:

  1. deps 中新增 //src/lib/fuchsia-sync
  2. 將程式碼中的 std::sync::Mutex 替換為 fuchsia_sync::Mutex
  3. std::sync::RwLock 替換為 fuchsia_sync::RwLock
  4. 移除遭汙染鎖定的任何錯誤處理作業,因為 fuchsia_sync 不支援鎖定汙染

啟用週期檢查

在偵錯版本中,這些檢查預設會在 fuchsia_sync 中啟用。

您可以在平衡或發布版本中手動啟用這些功能,方法是設定 GN arg:

fx set ... --args=fuchsia_sync_detect_lock_cycles=true

如果偵測到鎖定週期,您會看到類似以下的恐慌訊息:

thread 'main' (1) panicked at ../../third_party/rust_crates/forks/tracing-mutex-0.3.2/src/reporting.rs:
Found cycle in mutex dependency graph:
disabled backtrace

stack backtrace:
...

如要瞭解如何啟用回溯追蹤,請參閱下一節。

列印循環回溯

tracing-mutex 一律會列印導致實際觸發死結的恐慌執行緒回溯追蹤,但瞭解其他哪些鎖定取得作業屬於週期的一部分,通常也很有用。

RUST_BACKTRACE 環境變數設為 1 時,檢測工具會收集並列印這些額外的回溯。請注意,除了插碼的基準負擔外,這還會帶來大量的效能負擔。

如果是 ELF 元件,請在元件資訊清單中加入這個分片,以便收集所有鎖定取得作業的回溯追蹤,並在偵測到死結時列印相關項目:

{
  include: [ "//src/lib/fuchsia-sync/meta/enable_rust_backtrace.shard.cml" ],
  // ...
}

抑制恐慌

您可以呼叫

fuchsia_sync::suppress_lock_cycle_panics();

確保鎖定存取順序一致

本節列出一些策略,可供您在儀器偵測到週期後,用來避免死結。

範例

請參考下列程式碼:

fn do_thing_to_both(foo: Mutex<...>, bar: Mutex<...>) {
    let mut foo = foo.lock();
    let mut bar = bar.lock();
    foo.do_thing();
    bar.do_thing();
}

fn do_other_thing_to_both(foo: Mutex<...>, bar: Mutex<...>) {
    let mut bar = bar.lock();
    let mut foo = foo.lock();
    foo.do_other_thing();
    bar.do_other_thing();
}

fn main() {
    let foo = Mutex::new(...);
    let bar = Mutex::new(...);

    let first = std::thread::spawn(|| do_thing_to_both(foo, bar));
    let second = std::thread::spawn(|| do_other_thing_to_both(foo, bar));

    first.join().unwrap();
    second.join().unwrap();
}

如果事件依下列順序發生,這段程式碼就會發生死結:

  1. first收購 foo
  2. second收購 bar
  3. first 嘗試取得 bar,但 second 持有該物件
  4. second 嘗試取得 foo,但 first 持有該物件

步驟 (3) 和 (4) 會封鎖,且沒有任何執行緒可以喚醒這些步驟,導致死結。tracing-mutex 會恐慌,並顯示偵測到週期性的訊息。

視用途中鎖定的同步處理需求而定,您或許可以透過幾種方式避免這個週期。

移除重疊的鎖定取得作業

如要防止鎖定作業參與週期,最簡單的方法是在取得下一個鎖定之前釋放鎖定。如果兩個鎖定保護的值實際上不需要同步處理修改作業,這就很有用。

如要修正上述範例,請更新程式碼,如下所示:

fn do_thing_to_both(foo: Mutex<...>, bar: Mutex<...>) {
    {
        let mut foo = foo.lock();
        foo.do_thing();
    }
    {
        let mut bar = bar.lock();
        bar.do_thing();
    }
}

fn do_other_thing_to_both(foo: Mutex<...>, bar: Mutex<...>) {
    {
        let mut bar = bar.lock();
        bar.do_other_thing();
    }
    {
        let mut foo = foo.lock();
        foo.do_other_thing();
    }
}

// ...

在取得下一個鎖定之前先釋出每個鎖定,可確保沒有任何執行緒會無限期地讓其他執行緒處於飢餓狀態。

這樣一來,這兩個變數的修改作業就會交錯進行,但在許多情況下,這是可接受的。

調整門鎖存取順序

如果需要同步存取兩個以上的鎖定,請務必確保所有執行緒每次都以完全相同的順序取得鎖定。

在簡化範例中,您可以交換鎖定取得的順序,在 do_other_thing_to_both() 中達成這項操作:

fn do_thing_to_both(foo: Mutex<...>, bar: Mutex<...>) {
    // This order is the same as the original example.
    let mut foo = foo.lock();
    let mut bar = bar.lock();
    foo.do_thing();
    bar.do_thing();
}

fn do_other_thing_to_both(foo: Mutex<...>, bar: Mutex<...>) {
    // Now the code acquires the locks in the same order as do_thing_to_both().
    let mut foo = foo.lock();
    let mut bar = bar.lock();
    foo.do_other_thing();
    bar.do_other_thing();
}

// ...

一律先鎖定 foo 再鎖定 bar,可確保所有執行緒都以相同順序取得鎖定,避免形成週期和死結。

確認擷取順序正確

請盡可能在生命週期初期,以預期順序取得鎖定。 這會通知日後的讀者和週期儀器,瞭解正確的擷取順序,確保恐慌訊息的來源位置指向使用方式有誤的呼叫位置。

請將這些額外鎖定作業限制為啟用 debug_assertions 的建構版本,以免發布版本受到任何效能影響。

以兩個鎖為例,這表示在建立鎖後不久,就以所需順序取得這兩個鎖。例如:

fn main() {
    let foo = Mutex::new(...);
    let bar = Mutex::new(...);

    // foo should always be acquired before bar if they need to overlap.
    #[cfg(debug_assertions)]
    {
        let _foo = foo.lock();
        let _bar = bar.lock();
    }

    // ...
}

這樣可確保恐慌來自於 barfoo 之前取得的程式碼,無論受測邏輯的確切順序為何。