Rust in Everything
基于 lock_api 0.4.14

lock_api 自旋锁加 RawMutexTimed

主线位置:主线 1 的 raw 层换成自旋,外壳原样复用(给自己的锁套外壳)。再实现一个扩展 trait RawMutexTimed,外壳上就多出 try_lock_for / try_lock_until(raw trait 与契约)。代码在 exp-parking_lot/hands-on/src/spin.rs。

flowchart TB
  tlf["try_lock_for(d)"] --> dl{"Instant::now() + d 溢出?"}
  dl -->|"是"| forever["lock(),返回 true"]
  dl -->|"否"| tlu["try_lock_until(deadline)"]
  tlu --> t1{"try_lock() 成功?"}
  t1 -->|"是"| yes["返回 true"]
  t1 -->|"否"| late{"已过 deadline?"}
  late -->|"是"| no["返回 false"]
  late -->|"否"| rd["只读地等锁看起来空了,每 64 圈看一次时钟"]
  rd -->|"看起来空了"| t1
  rd -->|"过了 deadline"| no

RawMutex:test-and-test-and-set

pub struct RawSpinlock(AtomicBool);

unsafe impl RawMutex for RawSpinlock {
  const INIT: Self = RawSpinlock(AtomicBool::new(false));
  // unlock is a plain store: any thread may do it.
  type GuardMarker = GuardSend;

  fn lock(&self) {
    while !self.try_lock() {
      while self.is_locked() {
        spin_loop();
      }
    }
  }

  fn try_lock(&self) -> bool {
    self.0.compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed).is_ok()
  }

  unsafe fn unlock(&self) {
    self.0.store(false, Ordering::Release);
  }

  fn is_locked(&self) -> bool {
    self.0.load(Ordering::Relaxed)
  }
}

和 lock_api 文档里的例子比,lock 在 CAS 失败后先只读地等(spin 0.9.9 的写法),不让等待者每一轮都把缓存行抢成独占;is_locked 覆盖成一次 Relaxed 读,不用默认的「试锁一下」。unsafe impl 担保的仍是那三件事:CAS 保证互斥、Acquire / Release 成对、GuardSend 成立因为 unlock 不看线程。

RawMutexTimed:时间类型由实现者定

lock_api 是 no_std,trait 里的 Duration / Instant 是关联类型。这里在 std 环境,直接用 std 的:

unsafe impl RawMutexTimed for RawSpinlock {
  type Duration = Duration;
  type Instant = Instant;

  fn try_lock_for(&self, timeout: Duration) -> bool {
    match Instant::now().checked_add(timeout) {
      Some(deadline) => self.try_lock_until(deadline),
      // Overflow: treat as "no deadline", like parking_lot's util::to_deadline.
      None => {
        self.lock();
        true
      }
    }
  }

  fn try_lock_until(&self, deadline: Instant) -> bool {
    loop {
      if self.try_lock() {
        return true;
      }
      if Instant::now() >= deadline {
        return false;
      }
      let mut n = 0u32;
      while self.is_locked() {
        // reading the clock costs ~tens of ns; look at it every 64 spins
        n = n.wrapping_add(1);
        if n % 64 == 0 && Instant::now() >= deadline {
          return false;
        }
        spin_loop();
      }
    }
  }
}

三处细节:

  • 先试一次再看时钟:截止时间已过但锁是空的,仍然拿到锁。parking_lot 的 try_lock_for 也是先走一次快路径,失败才进 lock_slow(Some(deadline))(RawMutex 的一字节状态机)。
  • 溢出当作不限时:Duration::MAX 加到 Instant 上会溢出,parking_lot 的 util::to_deadline 用 checked_add 得到 None,等价于不设超时;这里照做。
  • 少读时钟:Instant::now() 在本机是几十纳秒(分辨率 41.7 ns),每圈都读会让自旋本身变慢,所以每 64 圈看一次。代价是超时最多晚 64 圈 spin_loop 的时间,在微秒以下。

实现了这个 trait,lock_api::Mutex<RawSpinlock, T> 上就自动有了 try_lock_for / try_lock_until,返回 Option<MutexGuard>。spin 0.9.9 只为自己的锁实现了 RawMutex,没有实现 RawMutexTimed / RawMutexFair(registry 里 spin-0.9.9/src 搜不到这两个名字),所以 spin::lock_api::Mutex 上没有带超时的加锁。

测试

tests/hands_on.rs 里四项,看门狗下运行:

测试做什么结果
spinlock_counts_exactly8 线程各加锁加一 10 万次,记临界区重叠恰好 800 000,重叠 0
spinlock_try_lock_for_times_out主线程持锁,另一线程 try_lock_for(20 ms)返回 None,耗时 20.0004 ms;断言 ≥ 20 ms 且 < 200 ms
spinlock_try_lock_for_succeeds_when_released_in_time持锁 30 ms 后放开,另一线程 try_lock_for(500 ms)拿到锁,耗时约 35 ms
spinlock_timeout_edge_cases空锁 + 过去的截止时间;Duration::MAX;被持有 + 截止时间就是现在前两个拿到锁;第三个立即返回 None(< 5 ms)

反向验证(feature broken_timed):try_lock_until 换成「只试一次」。3 次运行都是同样两项失败:spinlock_try_lock_for_times_out 报 gave up early: 41ns–84ns;spinlock_try_lock_for_succeeds_when_released_in_time 拿不到锁(83–166 ns 就返回了 false)。其余 6 项照常通过:普通 lock 路径没被改动。说明超时测试确实约束了「等够时间」和「等得到」两件事。

省掉了什么

省掉的parking_lot / 更完整的自旋锁会怎么做
退避crossbeam Backoff 指数退避后让出;parking_lot 的 SpinWait 自旋 3 轮、让出 7 轮后停车
停车持锁线程被调度走时,等待者在这里会空转整个时间片;要停车就把 raw 层换成 最小停车场 那样的写法
公平没有 RawMutexFair;要公平可以改成票据锁(ticket lock)
墙上时钟Instant 是单调时钟,不受系统时间调整影响;对照 parking_lot 0.12.5 在 Apple 上的超时(坑)

小结

  • lock_api 外壳下的 TTAS 自旋锁:CAS 失败后只读地等,is_locked 一次 Relaxed 读。
  • RawMutexTimed 的时间类型由实现者定;先试一次再看时钟、溢出当作不限时、每 64 圈读一次时钟。
  • 四项测试覆盖计数、超时下界、及时释放和边界;反向的「只试一次」让两项失败,其余照常通过。
  • spin 0.9.9 没有实现 RawMutexTimed,自己写一个只要几十行。

💬 关联讨论

发起讨论
加载中...