如何使用原子操作实现锁?

解读

在国内 Rust 后端/系统岗面试中,这道题常被用来区分“会用 Mutex”与“懂 Mutex 是怎么来的”
面试官期望你:

  1. 先讲清原子指令的可见性与顺序性(Acquire/Release/Sync);
  2. 再给出自旋锁的完整 Rust 实现(含内存序、忙等优化、公平性);
  3. 最后对比操作系统阻塞锁,说明何时该用哪一种。
    回答时务必手写代码,并解释每一步内存序为何不能省,这是国内大厂评分要点。

知识点

  • AtomicBoolcompare_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);
    }
}

关键点解释

  1. compare_exchange_weak 比 strong 更轻量,允许伪失败,在自旋场景性能更好。
  2. Ordering::Acquire 保证获取锁后,临界区内的读写不会被重排到加锁之前;Release 保证解锁前的写对后续获取者可见。
  3. spin_loop 在 x86 上会被编译成 PAUSE 指令,降低流水线冲突与功耗,面试提到这一点可加分
  4. Guard 模式把解锁逻辑放在 Drop 里,防止任何路径忘记释放锁,符合 Rust RAII 思想。

拓展思考

  1. 自旋锁 vs 阻塞锁
    临界区耗时 > 2 次上下文切换的场景,自旋锁浪费 CPU,应使用 std::sync::Mutex(内部基于 futex)。
  2. 公平性改造
    可引入 AtomicU32 票据队列,实现 FIFO,但代码量翻倍,适合追问“如何实现公平锁”
  3. 可重入需求
    需额外记录线程 ID 与计数,但会破坏无锁路径性能,嵌入式场景慎用
  4. no_std 环境
    裸机没有系统调用,只能依赖自旋锁;此时必须关闭中断或使用 ARM WFE/SEV 事件机制,面试提到“关中断”可展示系统功底