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_exactly | 8 线程各加锁加一 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,自己写一个只要几十行。