面试题答案
一键面试Rust读写锁与内存模型的相互作用
- 读写规则与可见性
- 在Rust中,
RwLock
的写操作会获取排他锁。当一个线程持有写锁进行写操作时,根据内存模型,这相当于一个释放(Release)操作。释放操作会将修改后的数据刷新到主内存,确保其他线程能够看到最新的数据。 - 读操作会获取共享锁。当一个线程持有读锁进行读操作时,这相当于一个获取(Acquire)操作。获取操作会从主内存中读取最新的数据,从而保证读操作能看到写操作的结果,确保了可见性。
- 在Rust中,
- 读写规则与顺序性
- 在多核处理器环境下,写操作的释放语义和读操作的获取语义通过内存屏障来保证顺序性。当一个线程持有写锁修改数据并释放锁时,内存屏障会确保所有之前的写操作都对其他线程可见。同样,当一个线程获取读锁时,内存屏障会确保该线程读取到的是最新的数据,且所有之前的读操作都已完成。
- 例如,假设线程A持有写锁修改了变量
x
,然后释放写锁。线程B获取读锁读取x
。内存屏障保证线程B读取到的x
是线程A修改后的值,且线程A对x
的修改在其释放写锁之前完成,线程B的读操作在获取读锁之后开始。
自定义高性能读写锁设计思路
- 设计思路
- 基于原子操作:利用Rust的
Atomic
类型,如AtomicUsize
,来管理锁的状态。通过原子操作来实现锁的获取和释放,避免使用标准库RwLock
中可能存在的一些额外开销。 - 减少锁竞争:对于高性能计算场景,读操作通常远多于写操作。可以采用一种类似“读者优先”的策略,减少读操作的等待时间。例如,维护一个计数器来记录当前活跃的读者数量,当有写操作请求时,只有当没有活跃读者时才允许写操作。
- 优化内存布局:合理安排锁的数据结构在内存中的布局,减少缓存争用。将与锁相关的数据紧凑地存储,使得在多核处理器环境下,不同线程对锁的访问能更高效地利用缓存。
- 基于原子操作:利用Rust的
- 关键代码片段
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
struct MyRwLock<T> {
data: T,
readers_count: AtomicUsize,
write_lock: AtomicUsize,
}
impl<T> MyRwLock<T> {
fn new(data: T) -> Arc<Self> {
Arc::new(Self {
data,
readers_count: AtomicUsize::new(0),
write_lock: AtomicUsize::new(0),
})
}
fn read(&self) -> Option<&T> {
loop {
while self.write_lock.load(Ordering::Acquire) != 0 {
std::thread::yield_now();
}
self.readers_count.fetch_add(1, Ordering::Acquire);
if self.write_lock.load(Ordering::Acquire) == 0 {
break;
}
self.readers_count.fetch_sub(1, Ordering::Release);
}
Some(&self.data)
}
fn write(&self) -> Option<&mut T> {
loop {
while self.readers_count.load(Ordering::Acquire) != 0 || self.write_lock.load(Ordering::Acquire) != 0 {
std::thread::yield_now();
}
if self.write_lock.compare_and_swap(0, 1, Ordering::Acquire) == 0 {
break;
}
}
Some(&mut self.data)
}
fn drop_read(&self) {
self.readers_count.fetch_sub(1, Ordering::Release);
}
fn drop_write(&self) {
self.write_lock.store(0, Ordering::Release);
}
}
在上述代码中,MyRwLock
通过AtomicUsize
类型的readers_count
和write_lock
来管理读写状态。read
方法用于获取读锁,write
方法用于获取写锁。drop_read
和drop_write
方法分别用于释放读锁和写锁。在获取锁时,通过while
循环和yield_now
方法来避免忙等待,提高性能。