面试题答案
一键面试可能导致死锁的常见情况
- 循环依赖:
- 多个线程按不同顺序获取锁。例如,线程A获取锁
Mutex1
,然后尝试获取锁Mutex2
;而线程B获取锁Mutex2
,然后尝试获取锁Mutex1
。如果这两个线程同时执行,就会形成死锁。 - 假设有两个结构体
ResourceA
和ResourceB
,分别由Mutex
保护:
use std::sync::{Mutex}; struct ResourceA { data: i32 } struct ResourceB { data: i32 } let mutex_a = Mutex::new(ResourceA { data: 0 }); let mutex_b = Mutex::new(ResourceB { data: 0 }); std::thread::spawn(move || { let _guard_a = mutex_a.lock().unwrap(); let _guard_b = mutex_b.lock().unwrap(); }); std::thread::spawn(move || { let _guard_b = mutex_b.lock().unwrap(); let _guard_a = mutex_a.lock().unwrap(); });
- 多个线程按不同顺序获取锁。例如,线程A获取锁
- 锁持有时间过长:
- 一个线程获取锁后,执行了长时间的计算或者I/O操作,而其他线程需要获取该锁才能继续执行,导致其他线程等待,若多个线程相互等待,就可能形成死锁。
- 例如,线程获取锁后进行大量的磁盘读写操作:
use std::sync::{Mutex}; use std::fs::File; use std::io::Read; let mutex = Mutex::new(0); std::thread::spawn(move || { let mut guard = mutex.lock().unwrap(); let mut file = File::open("large_file.txt").unwrap(); let mut buffer = String::new(); file.read_to_string(&mut buffer).unwrap(); // 这里进行大量磁盘读写操作,长时间持有锁 });
预防和检测死锁的方法
- 预防死锁:
- 按固定顺序获取锁:
- 在整个程序中,约定所有线程按照相同的顺序获取多个锁。例如,总是先获取
Mutex1
,再获取Mutex2
。 - 可以将需要获取锁的操作封装成函数,在函数内按固定顺序获取锁,避免不同线程以不同顺序获取锁。
- 在整个程序中,约定所有线程按照相同的顺序获取多个锁。例如,总是先获取
- 使用
try_lock
:Mutex
和RwLock
都提供了try_lock
方法。该方法尝试获取锁,如果锁不可用,立即返回Err
,而不是阻塞等待。- 可以在获取多个锁时,使用
try_lock
尝试获取每个锁。如果获取失败,可以释放已经获取的锁,然后重新尝试或者执行其他操作,避免死锁。 - 示例代码:
use std::sync::{Mutex}; let mutex_a = Mutex::new(0); let mutex_b = Mutex::new(0); std::thread::spawn(move || { match (mutex_a.try_lock(), mutex_b.try_lock()) { (Ok(guard_a), Ok(guard_b)) => { // 成功获取两个锁,进行操作 }, _ => { // 获取锁失败,释放已获取的锁(如果有) if let Ok(guard_a) = mutex_a.try_lock() { drop(guard_a); } if let Ok(guard_b) = mutex_b.try_lock() { drop(guard_b); } // 可以选择重新尝试获取锁或者执行其他操作 } } });
- 按固定顺序获取锁:
- 检测死锁:
- 使用
deadlock
crate:- 这是一个专门用于检测死锁的crate。可以在开发阶段引入该crate,它会在运行时检测死锁,并输出相关信息。
- 首先在
Cargo.toml
中添加依赖:deadlock = "0.4"
。 - 然后在代码中使用:
use deadlock::deadlock; let mutex_a = Mutex::new(0); let mutex_b = Mutex::new(0); deadlock::spawn(|| { let _guard_a = mutex_a.lock().unwrap(); let _guard_b = mutex_b.lock().unwrap(); }); deadlock::spawn(|| { let _guard_b = mutex_b.lock().unwrap(); let _guard_a = mutex_a.lock().unwrap(); }); deadlock::run();
- 使用
调试死锁问题的手段
- 日志输出:
- 在获取锁和释放锁的地方添加日志输出,记录线程ID、获取锁的时间等信息。通过分析日志,可以了解锁的获取顺序和等待情况,从而找出死锁的原因。
- 例如:
use std::sync::{Mutex}; use std::thread; let mutex = Mutex::new(0); thread::spawn(move || { println!("Thread {} is trying to lock mutex", thread::current().id()); let _guard = mutex.lock().unwrap(); println!("Thread {} locked mutex", thread::current().id()); // 操作 drop(_guard); println!("Thread {} released mutex", thread::current().id()); });
- 使用调试工具:
- GDB:
- 可以在GDB中运行程序,当程序死锁时,可以使用
bt
命令查看每个线程的堆栈信息。通过分析堆栈信息,找出线程正在等待的锁以及获取锁的顺序,从而定位死锁问题。
- 可以在GDB中运行程序,当程序死锁时,可以使用
- Rust标准库的
std::thread::dump_stack
:- 在代码中合适的位置(例如获取锁失败或者程序长时间无响应时)调用
std::thread::dump_stack
,它会输出当前线程的堆栈信息,有助于定位死锁。 - 示例:
use std::sync::{Mutex}; use std::thread; let mutex = Mutex::new(0); thread::spawn(move || { match mutex.lock() { Ok(_) => { // 操作 }, Err(_) => { println!("Failed to lock mutex, dumping stack:"); thread::dump_stack(); } } });
- 在代码中合适的位置(例如获取锁失败或者程序长时间无响应时)调用
- GDB: