鎖定依附元件週期是造成死結的常見原因。本指南提供相關操作說明,協助您偵測、偵錯及解決鎖定依附元件週期。
荒漠油廠
Fuchsia 上的 Rust 程式可以使用 fuchsia_sync 鎖定,從額外的執行階段檢查中獲益,偵測可能造成死結的存取模式。
這些檢查會依賴 tracing_mutex Crate,偵測不同執行緒間的鎖定取得作業週期。
採用 fuchsia_sync
如要在程式碼中使用 fuchsia_sync,請按照下列步驟操作:
- 在
deps中新增//src/lib/fuchsia-sync。 - 將程式碼中的
std::sync::Mutex替換為fuchsia_sync::Mutex。 - 將
std::sync::RwLock替換為fuchsia_sync::RwLock。 - 移除遭汙染鎖定的任何錯誤處理作業,因為
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();
}
如果事件依下列順序發生,這段程式碼就會發生死結:
first收購foosecond收購barfirst嘗試取得bar,但second持有該物件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();
}
// ...
}
這樣可確保恐慌來自於 bar 在 foo 之前取得的程式碼,無論受測邏輯的確切順序為何。