跳转至

构建自己的自旋锁

请求锁定常规互斥锁时, 若该锁已经处于锁定状态, 当成线程将进入睡眠状态, 等待操作系统的唤醒.
然而操作系统并不会在该锁被释放后立即唤醒线程, 所以这种方法会引入一定的延迟.

自旋锁 (spin lock) 是一种专门用于解决该问题的互斥锁. 这种锁不会让线程进入休眠状态, 而是会不断尝试上锁. 一旦锁被释放, 就会立即尝试锁定该锁.
由于需要不断的轮询, 这种方法会增加处理器的负担.

最小实现

pub struct SpinLock {
    locked: AtomicBool,
}

impl SpinLock {
    pub const fn new() -> Self {
        Self { locked: AtomicBool::new(false) }
    }

    pub fn lock(&self) {
        while self.locked.swap(true, Acquire) {
            std::hint::spin_loop();
        }
    }

    pub fn unlock(&self) {
        self.locked.store(false, Release);
    }
}

该实现可以确保在 unlock (Release) 和 lock (Acquire) 之间建立先行发生关系, 防止被锁保护的数据出现竞争.

其中 std::hint::spin_loop() 用于生成特殊的指令, 使处理器可以针对这种特殊用例做优化.

与 C++ 中的锁相似, 上面实现的锁并没有明确被保护的数据, 因此编译器无法判断上锁期间是否只访问了被保护的数据.
在此期间, 通过可变引用访问这些数据依然需要编写 unsafe 代码, 且需要人工检查是否只访问了被保护的数据.

这个问题可以通过让锁持有被保护的数据解决, 在编写锁的内部使用 unsafe 代码, 而不是任何使用了锁的地方.

与原子类型相同, 锁本身是不通过 mut 修饰的, 这样才能被不同线程共享. 但同时又需要所持有的数据是可变的, 这就需要借助内部可变性来实现:

use std::cell::UnsafeCell;

pub struct SpinLock<T> {
    locked: AtomicBool,
    value: UnsafeCell<T>,
}

因为 UnsafeCell 实现了 !Sync, 所以 SpinLock 默认不会实现 Sync, 需要手动为其添加实现:

unsafe impl<T> Sync for SpinLock<T> where T: Send {}

主动为类型添加自动 trait 通常意味着该复合类型中有类型不满足此 trait, 因此是 unsafe 代码.

评论