# rblock
A small `lock_api`-compatible read-biased `RwLock` for sharded, read-heavy
cache workloads.
This crate is intentionally narrow. It is not a general replacement for
`parking_lot::RwLock`; it is a policy choice for very short cache-shard critical
sections where read throughput matters more than bounded writer progress.
## What It Optimizes
The reader fast path only checks for an active writer. Waiting writers do not
close a reader gate, so new readers can keep entering while a writer is queued.
That avoids reader convoys in read-heavy mixed workloads.
The tradeoff is fairness. A constant stream of readers can delay a writer much
longer than a fair lock would. Use this when that tradeoff is explicit and
measured.
## When To Use It
| Read-heavy sharded cache, very short read/write critical sections | Use `rblock::RwLock` |
| Mostly reads with occasional small writes, latency target is read-side p99/p999 | Use `rblock::RwLock` |
| Write-heavy, skewed, or writer-tail-sensitive workload | Prefer `parking_lot::RwLock` or a fair policy |
| Long critical sections, IO while locked, blocking work while locked | Do not use this lock |
| General shared hash map where you want a ready-made concurrent map | Consider DashMap |
| Need bounded writer progress as a correctness or SLO property | Use `parking_lot::RwLock` |
## API
```rust
use rblock::RwLock;
let lock = RwLock::new(0usize);
{
let value = lock.read();
assert_eq!(*value, 0);
}
{
let mut value = lock.write();
*value += 1;
}
```
The exported guard types are:
- `RwLock<T>`
- `RwLockReadGuard<'_, T>`
- `RwLockWriteGuard<'_, T>`
## Benchmarks
This crate includes lock-only benchmarks. Run them directly from the crate root:
```bash
cargo bench --bench rwlock_load -- \
--duration-ms 500 \
--warmup-ms 150 \
--threads 1,2,4,8,16 \
--shards 1,16,64 \
--mixes 100-0,95-5,80-20,50-50,20-80,0-100 \
--latency-sample-rate 1024
cargo bench --bench writer_progress -- \
--duration-ms 500 \
--warmup-ms 150 \
--readers 1,2,4,8,16
```
For Linux runs, use the crate-local harness. It syncs only this crate into a
temporary workspace on the server:
```bash
./scripts/run-linux-bench.sh
BENCH=writer_progress \
./scripts/run-linux-bench.sh \
--duration-ms 500 \
--warmup-ms 150 \
--readers 1,2,4,8,16
```
For application-level A/B testing, compare your sharded data structure with this
lock against the same data structure using a fair lock. Record both throughput
and latency, including p50, p99, p999, read p999, and write p999. That matters:
this lock can improve ops/sec and read tails while making writer tails worse
under continuous readers.
### What The Load Benchmark Measures
`rwlock_load` is a lock-only benchmark. It creates an array of cache-padded lock
slots and runs the same workload against `rblock::RwLock<usize>` and
`parking_lot::RwLock<usize>`. Each operation chooses a shard uniformly at
random, then either:
- takes a read lock and reads the `usize`, or
- takes a write lock, increments the `usize`, and releases the lock.
There is no hash table, key comparison, allocation, eviction, TTL check, IO, or
application data structure in this benchmark. The point is to isolate lock
policy under controlled contention.
The `shards` setting controls how concentrated that contention is:
- `shards=1` is one hot lock. Every thread contends for the same lock, which
stresses the read-biased policy most directly.
- `shards=16` spreads operations uniformly over 16 independent locks. With 16
worker threads, the average contention per lock is much lower, so lock policy
matters less and scheduler/cache effects matter more.
- `shards=64` spreads the same threads over many more locks. This approximates a
well-striped cache where most operations do not collide on the same shard.
`ops/sec` is total completed lock operations per second across all worker
threads. The latency columns are sampled end-to-end operation latency for the
synthetic operation: shard choice, lock acquisition, the tiny critical section,
and unlock. They combine reads and writes according to the selected mix; they
are not separate read-latency and write-latency histograms.
## Representative Results
These are representative Linux results from May 14, 2026. They are
workload-specific; rerun the harness for your hardware and key distribution
before making a production decision.
### Lock-Only Load Benchmark
Command:
```bash
./scripts/run-linux-bench.sh \
--duration-ms 2000 \
--warmup-ms 500 \
--threads 1,2,4,8,16 \
--shards 1,16,64 \
--mixes 80-20,50-50,20-80 \
--latency-sample-rate 512
```
16 threads, one hot shard:
| 80/20 | 22.40M | 12.92M | 58.0us | 69.8us |
| 50/50 | 20.78M | 9.36M | 54.4us | 77.3us |
| 20/80 | 24.26M | 7.73M | 58.0us | 88.3us |
16 threads, 16 shards:
| 80/20 | 87.47M | 94.19M | 1.4us | 1.9us |
| 50/50 | 102.64M | 103.54M | 1.6us | 2.0us |
| 20/80 | 120.76M | 119.56M | 1.5us | 1.9us |
16 threads, 64 shards:
| 80/20 | 204.83M | 195.43M | 740ns | 670ns |
| 50/50 | 211.84M | 209.63M | 640ns | 640ns |
| 20/80 | 230.00M | 232.46M | 610ns | 590ns |
The single-shard case shows where the read-biased policy buys substantial
aggregate throughput under contention. Once contention is spread over many
shards, the result is much closer: `rblock` is competitive, but it is not a
universal throughput win over `parking_lot`.
### Writer Progress Warning
Lock-only writer-progress harness, 16 reader threads in a tight loop, one writer
thread repeatedly trying to acquire the write lock, zero synthetic work.
This is not a fixed offered-rate write workload. The writer thread runs as fast
as the lock lets it, so `writer acq/s` is a progress signal under reader
pressure. The latency columns are the main reason this benchmark exists.
Command:
```bash
BENCH=writer_progress \
./scripts/run-linux-bench.sh \
--duration-ms 2000 \
--warmup-ms 500 \
--readers 1,2,4,8,16
```
| 1 | 1.10M | 5.41M | 12.1us | 570ns |
| 2 | 220.0k | 4.20M | 53.9us | 1.4us |
| 4 | 125.9k | 2.36M | 65.0us | 5.1us |
| 8 | 10.3k | 1.41M | 1.4ms | 12.3us |
| 16 | 9 | 658.6k | 230.7ms | 46.6us |
This is the sharp edge. If writer progress or write p999 is the primary SLO,
use a fair lock policy.
### What These Numbers Mean
The load benchmark compares lock policy while keeping the synthetic sharded
design the same. It measures aggregate operation latency, not separate reader
and writer wait time.
That distinction matters most when readers are continuous. A read-biased lock
can raise aggregate throughput while making writer progress much worse. A fair
lock can improve writer tails at a throughput cost. A purpose-built concurrent
map may beat both if its exclusive path does less work than your full
cache/store path.
Use these results to decide whether `rblock` is worth testing in your design.
Use your own application benchmark to decide whether it is worth shipping.
## Production Guidance
Use this lock as a measured optimization, not as a default synchronization
primitive. The best fit is a striped cache where:
- shard count is high enough to spread contention,
- reads dominate,
- critical sections are CPU-only and very short,
- write latency is allowed to be less fair than read latency,
- benchmarks cover the real read/write mix and key distribution.
For write-heavy workloads, especially Zipf-skewed workloads, use the fair
`parking_lot` policy or a data structure with a smaller write path.