面试题答案
一键面试1. Mutex(互斥锁)
- 工作原理:Mutex 是“mutual exclusion”的缩写,它通过提供一个锁机制来确保同一时间只有一个线程可以访问被保护的数据。当一个线程获取了 Mutex 的锁,其他线程必须等待,直到该线程释放锁。在 Rust 中,Mutex 是一个智能指针类型
Mutex<T>
,对其内部数据T
的访问通过lock
方法来实现,该方法返回一个MutexGuard
,这是一个 RAII(Resource Acquisition Is Initialization)类型,当MutexGuard
离开作用域时,锁会自动释放。 - 适用场景:适用于需要独占访问共享资源的场景。例如,多个线程需要修改同一个共享的可变数据结构,如一个全局的
Vec<i32>
。 - 性能影响:由于同一时间只有一个线程能访问数据,可能会导致其他线程等待,从而降低并发性能。如果锁的持有时间过长,会加剧这种性能问题。在高并发场景下,如果频繁地获取和释放锁,会增加线程上下文切换的开销。
示例代码:
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut num = data.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final value: {}", *data.lock().unwrap());
}
2. RwLock(读写锁)
- 工作原理:RwLock 区分了读操作和写操作。允许多个线程同时进行读操作,因为读操作不会修改数据,所以不会产生数据竞争。但是,写操作需要独占访问,当有一个线程进行写操作时,其他读线程和写线程都必须等待。在 Rust 中,通过
read
方法获取读锁,返回RwLockReadGuard
;通过write
方法获取写锁,返回RwLockWriteGuard
,同样基于 RAII 机制来管理锁的生命周期。 - 适用场景:适用于读操作远远多于写操作的场景。例如,一个共享的配置文件,多个线程可能频繁读取配置,但很少修改它。
- 性能影响:读操作并发性能较好,因为多个读线程可以同时访问数据。然而,写操作时会阻塞所有读线程和其他写线程,若写操作频繁,会降低整体性能。同时,管理读写锁的状态本身也会带来一定的开销。
示例代码:
use std::sync::{RwLock, Arc};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(0));
let mut handles = vec![];
for _ in 0..5 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let num = data.read().unwrap();
println!("Read value: {}", num);
}));
}
for _ in 0..2 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
let mut num = data.write().unwrap();
*num += 1;
println!("Write value: {}", num);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
3. Condvar(条件变量)
- 工作原理:Condvar 用于线程间的同步通信,它通常与 Mutex 一起使用。一个线程可以在某个条件不满足时,通过 Condvar 进入等待状态,并释放它持有的 Mutex。当另一个线程改变了相关条件后,通过 Condvar 通知等待的线程,等待的线程被唤醒后重新获取 Mutex,然后检查条件是否满足。在 Rust 中,通过
wait
方法使线程等待,通过notify_one
或notify_all
方法通知等待的线程。 - 适用场景:适用于需要线程之间基于某个条件进行协作的场景。例如,生产者 - 消费者模型中,消费者线程在队列空时等待,生产者线程向队列中添加数据后通知消费者线程。
- 性能影响:如果频繁地进行等待和通知操作,会带来一定的开销,包括线程上下文切换和唤醒线程的开销。同时,如果没有正确使用,可能会导致虚假唤醒(spurious wakeup),即线程被唤醒但条件实际上并未满足,这需要在代码中进行额外的条件检查。
示例代码:
use std::sync::{Mutex, Condvar, Arc};
use std::thread;
fn main() {
let pair = Arc::new((Mutex::new(false), Condvar::new()));
let pair2 = Arc::clone(&pair);
let _ = thread::spawn(move || {
let (lock, cvar) = &*pair;
let mut started = lock.lock().unwrap();
*started = true;
drop(started);
cvar.notify_one();
});
let (lock, cvar) = &*pair2;
let started = lock.lock().unwrap();
let started = cvar.wait(started).unwrap();
assert!(started);
}