如何使用原子操作实现锁?
解读
在国内 Rust 后端/系统岗面试中,这道题常被用来区分“会用 Mutex”与“懂 Mutex 是怎么来的”。
面试官期望你:
- 先讲清原子指令的可见性与顺序性(Acquire/Release/Sync);
- 再给出自旋锁的完整 Rust 实现(含内存序、忙等优化、公平性);
- 最后对比操作系统阻塞锁,说明何时该用哪一种。
回答时务必手写代码,并解释每一步内存序为何不能省,这是国内大厂评分要点。
知识点
- AtomicBool 与 compare_exchange 的返回值语义
- Acquire/Release 配对建立** happens-before** 关系,防止编译器和 CPU 重排
- 自旋锁的忙等逻辑、指数退火、PAUSE 指令(x86)
- 公平性与不可重入问题:自旋锁通常不保证 FIFO,也不支持递归加锁
- Futex 机制:Linux 下把自旋锁升级为阻塞锁的必经之路(Rust parking_lot 内部做法)
- unsafe 边界:锁的实现必须手动保证临界区数据的 Send/Sync 语义
答案
下面给出生产级自旋锁的完整 Rust 实现,兼容 std 且零依赖,可直接在白板上写出:
use std::sync::atomic::{AtomicBool, Ordering};
use std::cell::UnsafeCell;
use std::ops::{Deref, DerefMut};
use std::hint::spin_loop;
pub struct SpinLock<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Sync for SpinLock<T> {}
unsafe impl<T: Send> Send for SpinLock<T> {}
impl<T> SpinLock<T> {
pub const fn new(value: T) -> Self {
SpinLock {
locked: AtomicBool::new(false),
data: UnsafeCell::new(value),
}
}
pub fn lock(&self) -> SpinLockGuard<T> {
// 快速路径:先尝试一次 CAS,减少 cache 颠簸
while self
.locked
.compare_exchange_weak(
false,
true,
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
// 慢速路径:纯自旋等待,CPU 提示降低功耗
while self.locked.load(Ordering::Relaxed) {
spin_loop();
}
}
SpinLockGuard { lock: self }
}
}
pub struct SpinLockGuard<'a, T> {
lock: &'a SpinLock<T>,
}
impl<'a, T> Deref for SpinLockGuard<'a, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.lock.data.get() }
}
}
impl<'a, T> DerefMut for SpinLockGuard<'a, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.lock.data.get() }
}
}
impl<'a, T> Drop for SpinLockGuard<'a, T> {
fn drop(&mut self) {
self.lock.locked.store(false, Ordering::Release);
}
}
关键点解释
- compare_exchange_weak 比 strong 更轻量,允许伪失败,在自旋场景性能更好。
- Ordering::Acquire 保证获取锁后,临界区内的读写不会被重排到加锁之前;Release 保证解锁前的写对后续获取者可见。
- spin_loop 在 x86 上会被编译成 PAUSE 指令,降低流水线冲突与功耗,面试提到这一点可加分。
- Guard 模式把解锁逻辑放在 Drop 里,防止任何路径忘记释放锁,符合 Rust RAII 思想。
拓展思考
- 自旋锁 vs 阻塞锁:
在临界区耗时 > 2 次上下文切换的场景,自旋锁浪费 CPU,应使用 std::sync::Mutex(内部基于 futex)。 - 公平性改造:
可引入 AtomicU32 票据队列,实现 FIFO,但代码量翻倍,适合追问“如何实现公平锁”。 - 可重入需求:
需额外记录线程 ID 与计数,但会破坏无锁路径性能,嵌入式场景慎用。 - no_std 环境:
裸机没有系统调用,只能依赖自旋锁;此时必须关闭中断或使用 ARM WFE/SEV 事件机制,面试提到“关中断”可展示系统功底。