3个不稳定版本
0.2.1 | 2024年3月21日 |
---|---|
0.2.0 | 2023年9月4日 |
0.1.0 | 2023年9月2日 |
在 并发 中排名 683
每月下载量 84
用于 2 个crates(通过 pstream)
33KB
320 行
vlock
此库包含快速且可扩展的多版本共享状态锁,具有无等待的读取访问。
许可
根据以下任一许可获得许可
- Apache License,版本2.0(LICENSE-APACHE 或 https://apache.ac.cn/licenses/LICENSE-2.0)
- MIT许可(LICENSE-MIT 或 http://opensource.org/licenses/MIT)
任选其一。
贡献
除非您明确声明,否则根据Apache-2.0许可证定义的,您提交的任何旨在包含在作品中的贡献,均应双许可如上所述,而不附加任何额外条款或条件。
lib.rs
:
类似于 std::sync::RwLock
,但它只锁定写操作,读取是无等待的。
VLock
用于读取密集型场景。更新是严格序列化的,可能需要等待较旧数据的读取者完成。它通过保留共享数据的多个版本,并对每个版本的引用进行计数来工作。命名为 VLock
,因为它使用版本控制和类似锁的行为。命名很难。
为什么要这么做?
VLock
是一种快速且可扩展的锁,以牺牲较慢的写操作为代价,在非复制类型上具有稳定的读取性能。
这在各种类型的系统中是一个常见的模式,其中有许多处理线程,每个线程通过共享状态和偶尔更新该共享状态的其他代码来做出决定。例如,在网络基础设施中,这可以被认为是数据平面和控制平面,无论是路由器还是各种级别的负载均衡器。在数据密集型应用程序中,这可能是一些存储在数据库中的东西的热视图。
如果这大致描述了您的使用场景,您可能会从这个库中获得好处,但代价是内存中额外的1+份数据副本、稍多的状态和较慢的写入性能。即使使用RwLock
来最小化加锁时间,以及VLock
实际上重新使用旧版本,这可能会通过避免分配来为您节省一些时间,这种情况也是必要的。额外的状态是一个usize
加上每个版本一个usize
……至于较慢的写入,您无能为力。
如果读取性能至关重要,且读者不能等待写入,那么您可能会获得显著的好处。
此外,实现简单,大约有200行实际代码,且没有任何外部依赖。
[^1]: 基于x86_64
笔记本电脑上的合成基准测试,读取性能比ArcSwap
快1.3-2.0倍,在某些情况下可能比最快的RwLock
实现快一个数量级。与ArcSwap
相比,VLock
的写入更有效。与RwLock
相比,写入通常比parking_lot
实现慢1到10倍,但优于std
实现。与SeqLock
相比,结果不一:在某些场景中,VLock
的读取慢4倍,在某些情况下约为1:1,在其他情况下快2倍。尽管VLock
的写入性能比SeqLock
差得多,但它可以用于非复制类型。请注意,当读者没有进展且N
较小时,VLock
的写入性能可能会显著下降,换句话说,VLock
容易因为优先读取而导致写入饥饿。
它是如何工作的?
如上所述,它使用版本和引用计数与锁的组合来序列化写入。
[VLock<T, N>
]有一个固定大小的N
,这是可以分配的最大版本数。版本由分配空间中的偏移量标识。状态同时包含偏移量和同一原子中的计数器。VLock
保留当前状态和每个版本的每个状态。
每次调用read
时,当前状态计数器递增,并使用关联的偏移量将当前版本返回给读者。返回的指针释放后,每个版本的状态计数器递减。
在update
期间,通过检查每个版本计数器是否为0来选择第一个未使用的版本,如果没有可用,如果大小允许,则初始化新版本。这就是等待读者的地方。一旦找到空闲偏移量并更新,当前状态计数器重置,并写入新偏移量(这是一次原子操作)。然后,将返回的计数器添加到与该版本关联的每个版本计数器中,之后它将达到0,一旦所有读者都处理完毕。
换句话说,跟踪访问可以通过两个计数器来考虑平衡——一个在当前状态中递增,标记数据读取访问的开始,另一个在每个版本的状态中递减,标记访问的结束。然后,将它们相加。如果结果计数器大于0,则仍有读者,如果为0,则版本肯定未使用。
旧版本不会被删除,因此它就像一个池。在处理需要更新或重建的堆分配数据结构时可能很有用,但也可能导致一些旧垃圾数据留在内存中。
用法
use std::sync::Arc;
use std::thread;
use std::time;
use vlock::VLock;
let lock = Arc::new(VLock::<String, 4>::new(String::from("hi there!")));
let lock_clone = Arc::clone(&lock);
let t = thread::spawn(move || {
for _ in 0..5 {
println!("{}", *lock_clone.read());
thread::sleep(time::Duration::from_millis(1));
}
lock_clone.update(
|_, value| {
value.clear();
value.push_str("bye!");
},
String::new
);
});
thread::sleep(time::Duration::from_millis(2));
lock.update(
|_, value| {
value.clear();
value.push_str("here's some text for you");
},
String::new
);
if let Err(err) = t.join() {
println!("thread has failed: {err:?}");
}
assert_eq!(*lock.read(), "bye!");