和 std RwLock 比
主线位置:主线 1 的读写锁对照组。parking_lot 的写者先占
WRITER_BIT截断读者流,再在addr + 1上等读者走光;本机 std 的queue.rs是另一套写者优先的实现。这一页比较两者的结构,并量一个 crossbeam 卷没有量过的数:读者流不断时,写者要等多久。
flowchart LR
subgraph pl["parking_lot::RwLock<T>"]
s1["AtomicUsize:标志位 + 读者数"] --> q1["停车场:addr 与 addr+1 两条队列"]
end
subgraph mac["std::sync::RwLock<T>,macOS:queue.rs"]
s2["AtomicPtr:标志位 + 等待节点链表指针"] --> q2["链表节点在等待线程的栈上"]
end
subgraph lx["std::sync::RwLock<T>,Linux:futex.rs"]
s3["state: u32(30 位读者数 + 两个等待位)"] --> k["内核 futex"]
s4["writer_notify: u32"] --> k
end
std 的两种实现
rust-src 1.99.1 的 sys/sync/rwlock/mod.rs:1-27:Linux、Android、BSD、Windows(非 win7)等用 futex.rs;其余 unix(本机 macOS)用 queue.rs。和 Mutex 不同,macOS 上的 RwLock 不用 pthread_rwlock_t。
queue.rs:模块文档(1-31 行)列了不用 pthread 读写锁的理由(不可移动要堆分配、不能内联、某些平台有 bug、「On some platforms (e.g. macOS), the lock is very slow」),实现策略照搬 Windows SRWLOCK 和 usync 库:状态是一个指针,低 4 位是LOCKED/QUEUED/QUEUE_LOCKED/DOWNGRADED,其余位指向等待节点的链表,节点放在等待线程自己的栈上。文档自述的性质:自适应(先有限地自旋)、除了每线程的Thread句柄之外不分配、「writer-preferring, even if some readers may still slip through」、「unfair」。读锁的快路径只在没有QUEUED时尝试(152 行):有人排队,新读者就去排队。futex.rs:两个u32(4-15 行),state里 30 位读者数、一个「有读者在等」位、一个「有写者在等」位,writer_notify专门用来叫醒写者。
std 自己的文档不承诺任何优先策略:「The priority policy of the lock is dependent on the underlying operating system's implementation, and this type does not guarantee that any particular policy will be used」,并给了同线程重复读可能死锁的例子(sync/poison/rwlock.rs:22-38);read() 的文档另写「might panic when called if the lock is already held by the current thread」(385-386 行)。parking_lot 则把策略写死在文档里:task-fair,有写者在等时读者阻塞(读、写与写者优先)。
| parking_lot | std,macOS(queue.rs) | std,Linux(futex.rs) | |
|---|---|---|---|
RwLock<()> 大小 | 8 | 16(实测) | 12(编译期断言) |
RwLock<u64> 大小 | 16 | 24(实测) | 24(编译期断言) |
| 等待队列 | 全局停车场,两个 key | 等待线程栈上的链表 | 内核 |
| 写者在等时新读者 | 阻塞(read_recursive 例外) | 文档称写者优先,「some readers may still slip through」 | 未在本卷核实 |
| 升级 / 降级 | 可升级读、三种降级 | 只有写 → 读的 downgrade(1.92.0 起稳定) | 同左 |
| 毒化 | 无 | 写者 panic 时毒化 | 同左 |
大小来自 E1(和 std Mutex 比那页的同一个实验)。std 的 RwLockWriteGuard::downgrade 在 rust-src 1.99.1 里标着 #[stable(feature = "rwlock_downgrade", since = "1.92.0")](sync/poison/rwlock.rs:882-883);parking_lot 的 downgrade 早就有,另有 std 没有的可升级读。
吞吐:引用 crossbeam 卷
crossbeam 卷的 ShardedLock 页已经在本机用 12 个线程扫过读写比例:只读时 std RwLock 约 2.1–2.3 百万次 / 秒、parking_lot 5.7(两次运行相同);全写时 std 25.4–29.3、parking_lot 35.9。这里不重测。那一页的推测「只读时所有线程同时改同一个计数」,在 parking_lot 这一侧可以从代码确认:每次 read() 都对同一个状态字做 CAS,失败就 spin_no_yield 退避重试(读、写与写者优先)。那组数字所用的实验 crate(exp-crossbeam/sharded-lock)依赖 parking_lot = "0.12"、锁文件里是 0.12.5,只开默认 feature,没有 deadlock_detection,和本卷的计时规矩一致,可以直接引用。
实验 E4:读者流不断时,写者等多久
断言:parking_lot 的写者不会被持续的读者流饿死,等待时间在「一个读临界区 + 一次唤醒」的量级;反过来让读者用 read_recursive(允许插到等待中的写者前面),写者应当明显等得更久——这是测法本身的反向验证。
方法(exp-parking_lot/rwlock-writer,0.13.0 列在 exp-parking_lot/v013-rwlock-writer,同一份源码,都不开 deadlock_detection):11 个读者线程循环「read() → 忙等 1 µs → 解锁」,锁外不做事;1 个写者线程每 1 ms 申请一次写锁,记录从调用 write() 到拿到锁的时间;跑 3 秒后读者停止。三种读法:parking_lot read()、read_recursive()(0.13.0 没有这个方法,换成 RecursiveRwLock::read())、std read()。再加一组「没有读者」作为下限。M2 Max(8P + 4E),每组 3 轮。
// writer thread
while !stop.load(Ordering::Relaxed) {
next += Duration::from_millis(1);
sleep_until(next);
let t = Instant::now();
lock.write_inc(); // *lock.write() += 1
waits.push(t.elapsed().as_nanos() as u64);
}
结果(22:37–22:38,开始时 1 / 5 分钟负载 2.67 / 2.98,结束时被实验自己推到 7.47 / 4.45)。每格是 3 轮的中位数,括号里是 3 轮的范围;每轮写者约做 3000 次写:
| 实现 | 读法 | 写者等待 p50 | p99 | 最大 | 读者吞吐(百万次 / 秒) |
|---|---|---|---|---|---|
| 0.12.5 | read() | 5.9 µs(5.8–8.6) | 90 µs(84–99) | 1.1 ms(0.9–2.0) | 5.16 |
| 0.13.0 | read() | 7.1 µs(5.3–7.5) | 95 µs(81–159) | 2.1 ms(1.1–10.0) | 4.98 |
std(queue.rs) | read() | 36.0 µs(35.6–36.2) | 1.7 ms(1.4–2.6) | 11.6 ms(10.0–19.4) | 3.24 |
| 0.12.5 | read_recursive() | 280 µs(268–307) | 5.2 ms(4.0–5.4) | 52 ms(41–56) | 5.20 |
| 0.13.0 | RecursiveRwLock::read() | 258 µs(241–262) | 3.2 ms(3.2–8.1) | 27 ms(16–62) | 4.85 |
| 0.12.5 / std | 没有读者 | ≤ 0.04 µs | 0.1–0.2 µs | ≤ 11 µs | — |
- parking_lot 的写者不会被读者流饿死:p50 约 6 µs,大约是「等手上这批 1 µs 的读临界区结束 + 一次唤醒」;p99 不到 0.1 ms。0.13.0 与 0.12.5 在这一项上没有可分辨的差别。
- 反向验证成立:读者改用
read_recursive(或 0.13.0 的RecursiveRwLock)后,写者等待的中位数涨了约 45 倍,最大值到几十毫秒。11 个读者此起彼伏,读者数很少降到 0,递归读一直放读者进来,写者只能等到所有读者恰好同时不在锁里。这说明上面 6 µs 的结果确实来自「WRITER_BIT挡住新读者」,而不是测法本身看不出差别。 - 本机 std 介于两者之间:p50 36 µs,p99 和最大值比 parking_lot 高一个数量级以上,读者吞吐也低约 37%。
queue.rs自述「writer-preferring, even if some readers may still slip through」,数字和这句话一致:写者最终总能进去,但会被溜进来的读者拖慢。 - 「没有读者」一组是下限:写锁的快路径一次 CAS,测到的就是
Instant::now()的分辨率。
小结
- std 的
RwLock在 macOS 上是 std 自己的queue.rs(写者优先、不公平、节点在等待线程栈上),在 Linux 上是两个u32的 futex 实现;parking_lot 是一个字加停车场的两条队列。 - 大小:parking_lot
RwLock<()>8 字节,std 在 macOS 上 16、Linux 上 12。 - 吞吐直接引用 crossbeam 卷:12 线程只读时 parking_lot 约为 std 的 2.5–2.7 倍,全写时 1.2–1.4 倍。
- E4(本机,11 个读者 + 每 1 ms 一次写):parking_lot 写者等待 p50 约 6 µs、p99 约 0.1 ms;本机 std p50 36 µs、p99 约 1.7 ms;读者改用
read_recursive后 p50 约 0.28 ms、最大 52 ms(反向验证)。 - 同线程重复读:两边都会死锁(E5,读、写与写者优先);parking_lot 有
read_recursive可以绕开,代价是写者可能等得很久。