RwLock允许多个读者并发访问,但“允许并发”不等于“读取路径没有竞争”。Left-Right 用两份数据、操作日志和显式发布,把大部分协调成本从读者转移给写者。本文从缓存一致性、算法流程和 Rust 实现三个层面解释这笔交换何时值得。
RwLock 开始假设服务中有一份商品价格表:请求线程频繁查询价格,后台线程偶尔更新价格。最直接的实现是:
use std::{collections::BTreeMap, sync::RwLock};
struct Catalog {
prices: RwLock<BTreeMap<u64, u64>>,
}
impl Catalog {
fn price(&self, id: u64) -> Option<u64> {
self.prices.read().unwrap().get(&id).copied()
}
fn upsert(&self, id: u64, price: u64) {
self.prices.write().unwrap().insert(id, price);
}
}
这段代码容易理解,也通常是正确的起点。RwLock 的读锁可以被多个线程同时持有,因此它并没有把所有读取串行化。
问题出现在高并发下:获取和释放读锁仍然需要协调共享状态。多个 CPU 核会反复更新或观察同一块锁元数据,对应的缓存行会在核心之间迁移。数据查询可能只是一次很快的 BTreeMap::get,锁状态的原子操作和缓存一致性流量反而逐渐成为热点。
因此,准确的说法不是“RwLock 的读取不能并行”,而是:
RwLock的读取能够并行,但读者仍在同一个同步点上协调。
此外,写者到来时,新读者是否继续进入、写者是否优先等策略依赖具体实现和操作系统。平均延迟可能不错,尾延迟却会随竞争上升。
Left-Right 同时维护两份逻辑相同的数据,通常称为 Left 和 Right:
publish,原子切换读者所访问的副本。flowchart LR
R1["Reader 1"] --> P["active pointer"]
R2["Reader 2"] --> P
RN["Reader N"] --> P
P --> L["Left: 当前读副本"]
W["Single writer"] --> O["operation log"]
O --> RR["Right: 当前写副本"]
W -. "publish / flip" .-> P
一次发布可以分成下面几个阶段: