What are the trade-offs between parking_lot::RwLock and std::sync::RwLock for read-heavy workloads?
parking_lot::RwLock uses a fair scheduling algorithm with condition variable-based parking that prevents writer starvation while maintaining good read throughput, whereas std::sync::RwLock on Unix systems allows readers to acquire the lock even when a writer is waiting, potentially causing writer starvation in read-heavy workloads. The key trade-offs are fairness versus raw read throughput: parking_lot::RwLock queues readers behind waiting writers, ensuring writers eventually acquire the lock, while std::sync::RwLock prioritizes readers, allowing high read throughput but potentially blocking writers indefinitely. Additionally, parking_lot::RwLock offers smaller memory footprint, faster uncontended operations, and API improvements like RawRwLock for lock-free checked access, but requires an external dependency.
Basic RwLock Usage Comparison
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
fn main() {
// std::sync::RwLock
let std_lock = StdRwLock::new(42);
{
let read_guard = std_lock.read().unwrap();
println!("std read: {}", *read_guard);
}
{
let write_guard = std_lock.write().unwrap();
println!("std write: {}", *write_guard);
}
// parking_lot::RwLock
let pl_lock = PlRwLock::new(42);
{
let read_guard = pl_lock.read();
println!("parking_lot read: {}", *read_guard);
}
{
let write_guard = pl_lock.write();
println!("parking_lot write: {}", *write_guard);
}
}Both provide similar APIs, but parking_lot returns guards directly without Result, while std returns Result due to potential poisoning.
Lock Poisoning Differences
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
use std::panic;
fn main() {
// std::sync::RwLock: Poisoning support
let std_lock = StdRwLock::new(42);
// If a thread panics while holding the lock, it becomes "poisoned"
let result = panic::catch_unwind(|| {
let mut guard = std_lock.write().unwrap();
panic!("intentional panic");
});
// Subsequent access returns Err (poisoned)
match std_lock.read() {
Ok(guard) => println!("std read succeeded: {}", *guard),
Err(e) => println!("std read failed (poisoned): {}", e),
}
// parking_lot::RwLock: No poisoning
let pl_lock = PlRwLock::new(42);
let _ = panic::catch_unwind(|| {
let mut guard = pl_lock.write();
panic!("intentional panic");
});
// Subsequent access still works
let guard = pl_lock.read();
println!("parking_lot read after panic: {}", *guard);
}std::sync::RwLock tracks poisoning; parking_lot::RwLock does not, simplifying error handling.
Read-Heavy Workload Performance
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
use std::thread;
use std::time::Instant;
fn benchmark_std_rwlock() {
let lock = StdRwLock::new(0u64);
let read_iterations = 1_000_000;
let write_iterations = 1_000;
let start = Instant::now();
// Multiple reader threads
let readers: Vec<_> = (0..4)
.map(|_| {
let lock = &lock as *const StdRwLock<u64>;
thread::spawn(move || {
let lock = unsafe { &*lock };
for _ in 0..read_iterations {
let guard = lock.read().unwrap();
let _ = *guard;
}
})
})
.collect();
// Single writer thread
let writer = {
let lock = &lock as *const StdRwLock<u64>;
thread::spawn(move || {
let lock = unsafe { &*lock };
for _ in 0..write_iterations {
let mut guard = lock.write().unwrap();
*guard += 1;
}
})
};
for r in readers { r.join().unwrap(); }
writer.join().unwrap();
println!("std::sync::RwLock: {:?}", start.elapsed());
}
fn benchmark_parking_lot_rwlock() {
let lock = PlRwLock::new(0u64);
let read_iterations = 1_000_000;
let write_iterations = 1_000;
let start = Instant::now();
let readers: Vec<_> = (0..4)
.map(|_| {
let lock = &lock as *const PlRwLock<u64>;
thread::spawn(move || {
let lock = unsafe { &*lock };
for _ in 0..read_iterations {
let guard = lock.read();
let _ = *guard;
}
})
})
.collect();
let writer = {
let lock = &lock as *const PlRwLock<u64>;
thread::spawn(move || {
let lock = unsafe { &*lock };
for _ in 0..write_iterations {
let mut guard = lock.write();
*guard += 1;
}
})
};
for r in readers { r.join().unwrap(); }
writer.join().unwrap();
println!("parking_lot::RwLock: {:?}", start.elapsed());
}Performance characteristics differ based on contention patterns and platform implementation.
Writer Starvation Behavior
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
use std::thread;
use std::sync::atomic::{AtomicUsize, Ordering};
fn demonstrate_writer_starvation_std() {
// std::sync::RwLock on Linux may allow readers to continuously
// acquire the lock even when a writer is waiting
// This can cause "writer starvation"
let lock = StdRwLock::new(0);
let read_count = AtomicUsize::new(0);
let write_count = AtomicUsize::new(0);
// Continuously reading
let reader = {
let lock = &lock;
let read_count = &read_count;
thread::spawn(move || {
for _ in 0..10000 {
if let Ok(guard) = lock.read() {
let _ = *guard;
read_count.fetch_add(1, Ordering::Relaxed);
}
}
})
};
// Trying to write
let writer = {
let lock = &lock;
let write_count = &write_count;
thread::spawn(move || {
for _ in 0..100 {
if let Ok(mut guard) = lock.write() {
*guard += 1;
write_count.fetch_add(1, Ordering::Relaxed);
}
}
})
};
reader.join().unwrap();
writer.join().unwrap();
println!("std reads: {}, writes: {}",
read_count.load(Ordering::Relaxed),
write_count.load(Ordering::Relaxed));
}
fn demonstrate_fairness_parking_lot() {
// parking_lot::RwLock uses fair queuing
// Writers will eventually acquire the lock
let lock = PlRwLock::new(0);
let read_count = AtomicUsize::new(0);
let write_count = AtomicUsize::new(0);
let reader = {
let lock = &lock;
let read_count = &read_count;
thread::spawn(move || {
for _ in 0..10000 {
let guard = lock.read();
let _ = *guard;
read_count.fetch_add(1, Ordering::Relaxed);
}
})
};
let writer = {
let lock = &lock;
let write_count = &write_count;
thread::spawn(move || {
for _ in 0..100 {
let mut guard = lock.write();
*guard += 1;
write_count.fetch_add(1, Ordering::Relaxed);
}
})
};
reader.join().unwrap();
writer.join().unwrap();
println!("parking_lot reads: {}, writes: {}",
read_count.load(Ordering::Relaxed),
write_count.load(Ordering::Relaxed));
}parking_lot::RwLock ensures fairness; std::sync::RwLock may starve writers.
Memory Footprint Comparison
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
use std::mem::size_of;
fn main() {
// Size comparison
// parking_lot::RwLock is typically smaller
struct StdData {
lock: StdRwLock<()>,
}
struct PlData {
lock: PlRwLock<()>,
}
println!("std::sync::RwLock<()> size: {}", size_of::<StdRwLock<()>>());
println!("parking_lot::RwLock<()> size: {}", size_of::<PlRwLock<()>>());
// On most platforms:
// std::sync::RwLock: varies by platform, often larger
// parking_lot::RwLock: typically smaller, consistent size
// For many locks in a data structure, this matters:
struct CacheStd {
entries: Vec<StdRwLock<String>>,
}
struct CachePl {
entries: Vec<PlRwLock<String>>,
}
// CachePl uses less memory per entry
}parking_lot::RwLock has smaller, consistent memory footprint across platforms.
API Differences: Upgradable Reads
use parking_lot::RwLock as PlRwLock;
use std::sync::RwLock as StdRwLock;
fn main() {
// parking_lot supports upgradable reads
let pl_lock = PlRwLock::new(vec![1, 2, 3]);
// Upgradable read: can be upgraded to write
{
let upgradable = pl_lock.upgradable_read();
if upgradable.len() < 10 {
// Upgrade to write without releasing
let mut write_guard = upgradable.upgrade();
write_guard.push(4);
}
// If not upgraded, automatically releases as read
}
// std::sync::RwLock does not support upgradable reads directly
// You must release the read lock and acquire write lock
let std_lock = StdRwLock::new(vec![1, 2, 3]);
{
let read_guard = std_lock.read().unwrap();
if read_guard.len() < 10 {
drop(read_guard); // Must release first
let mut write_guard = std_lock.write().unwrap();
write_guard.push(4);
}
}
}parking_lot::RwLock supports upgradable reads; std::sync::RwLock requires releasing the read lock first.
API Differences: try_lock Variants
use parking_lot::RwLock;
use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
let lock = RwLock::new(42);
// Try to acquire read lock without blocking
match lock.try_read() {
Some(guard) => println!("Got read lock: {}", *guard),
None => println!("Read lock not available"),
}
// Try to acquire write lock without blocking
match lock.try_write() {
Some(guard) => println!("Got write lock"),
None => println!("Write lock not available"),
}
// Try to acquire upgradable read
match lock.try_upgradable_read() {
Some(guard) => {
// Can upgrade if needed
if *guard == 42 {
let mut write_guard = guard.upgrade();
*write_guard = 100;
}
}
None => println!("Upgradable read not available"),
}
}parking_lot::RwLock returns Option<Guard> for try methods; std returns Result.
RawRwLock for Low-Level Access
use parking_lot::{RwLock, RawRwLock};
fn main() {
// RawRwLock provides lock-free checked access
let lock = RwLock::new(42);
// Check if lock is available without acquiring
// This is useful for certain optimization patterns
// parking_lot exposes the raw lock type
// RawRwLock can be used in lock-free data structures
// Example: Check before potentially blocking operation
if lock.try_read().is_some() {
// Fast path: we got the lock immediately
let guard = lock.read();
println!("Value: {}", *guard);
} else {
// Slow path: consider alternative approach
println!("Lock contended, doing something else");
}
}parking_lot exposes RawRwLock for low-level lock-free programming patterns.
Contention Performance Under Load
use parking_lot::RwLock as PlRwLock;
use std::sync::RwLock as StdRwLock;
use std::thread;
use std::time::Instant;
fn contention_benchmark() {
const NUM_THREADS: usize = 8;
const ITERATIONS: usize = 100_000;
// Test with high contention
fn test_std() -> u64 {
let lock = StdRwLock::new(0u64);
let start = Instant::now();
let threads: Vec<_> = (0..NUM_THREADS)
.map(|i| {
let lock = &lock as *const StdRwLock<u64>;
thread::spawn(move || {
let lock = unsafe { &*lock };
for _ in 0..ITERATIONS {
if i % 4 == 0 {
// Write
let mut guard = lock.write().unwrap();
*guard += 1;
} else {
// Read
let guard = lock.read().unwrap();
let _ = *guard;
}
}
})
})
.collect();
for t in threads { t.join().unwrap(); }
start.elapsed().as_micros() as u64
}
fn test_parking_lot() -> u64 {
let lock = PlRwLock::new(0u64);
let start = Instant::now();
let threads: Vec<_> = (0..NUM_THREADS)
.map(|i| {
let lock = &lock as *const PlRwLock<u64>;
thread::spawn(move || {
let lock = unsafe { &*lock };
for _ in 0..ITERATIONS {
if i % 4 == 0 {
let mut guard = lock.write();
*guard += 1;
} else {
let guard = lock.read();
let _ = *guard;
}
}
})
})
.collect();
for t in threads { t.join().unwrap(); }
start.elapsed().as_micros() as u64
}
println!("std: {} µs", test_std());
println!("parking_lot: {} µs", test_parking_lot());
}Contention patterns significantly affect which implementation performs better.
Platform-Specific Behavior
use std::sync::RwLock as StdRwLock;
fn main() {
// std::sync::RwLock behavior differs by platform:
// Linux (glibc pthread_rwlock):
// - Readers can acquire even when writer is waiting
// - Good for read throughput
// - Can starve writers
// macOS (pthread_rwlock):
// - Different fairness policy
// - Writers may get more priority
// Windows (SRWLock):
// - Different implementation entirely
// - May have different fairness characteristics
// parking_lot::RwLock:
// - Consistent behavior across all platforms
// - Fair scheduling (FIFO-ish)
// - Predictable performance characteristics
let lock = StdRwLock::new(42);
// Behavior on Linux vs macOS vs Windows may differ
}std::sync::RwLock behavior varies by platform; parking_lot is consistent.
When to Use Each
// Use parking_lot::RwLock when:
// 1. You need consistent cross-platform behavior
// 2. Writer fairness matters (avoiding writer starvation)
// 3. You need upgradable reads
// 4. Memory footprint matters (many locks)
// 5. You want to avoid lock poisoning complexity
// 6. You need fast uncontended operations
// Use std::sync::RwLock when:
// 1. You don't want external dependencies
// 2. You specifically want the platform's native behavior
// 3. Lock poisoning is important for your error handling
// 4. You're in a pure read-heavy workload where writer fairness doesn't matter
// 5. You need the standard library's semantics guarantees
fn choose_rwlock() {
// Example decision tree:
// Need upgradable reads? -> parking_lot
// Need consistent cross-platform behavior? -> parking_lot
// Need poisoning for crash recovery? -> std
// Minimizing dependencies matters most? -> std
// Fair writer scheduling matters? -> parking_lot
}The choice depends on your specific requirements around fairness, platform consistency, and features.
Real-World Example: Read-Heavy Cache
use parking_lot::RwLock;
use std::collections::HashMap;
use std::hash::Hash;
struct Cache<K, V> {
data: RwLock<HashMap<K, V>>,
}
impl<K: Eq + Hash + Clone, V: Clone> Cache<K, V> {
fn new() -> Self {
Cache {
data: RwLock::new(HashMap::new()),
}
}
// Read-heavy: many reads, few writes
fn get(&self, key: &K) -> Option<V> {
let guard = self.data.read();
guard.get(key).cloned()
}
// Occasional write
fn insert(&self, key: K, value: V) {
let mut guard = self.data.write();
guard.insert(key, value);
}
// Upgradable read: check then potentially modify
fn get_or_insert<F>(&self, key: K, f: F) -> V
where
K: Eq + Hash,
F: FnOnce() -> V,
V: Clone,
{
// First try read
{
let guard = self.data.read();
if let Some(v) = guard.get(&key) {
return v.clone();
}
}
// Then try upgradable read
let guard = self.data.upgradable_read();
if let Some(v) = guard.get(&key) {
return v.clone();
}
// Upgrade to write
let mut guard = guard.upgrade();
let value = f();
guard.insert(key.clone(), value.clone());
value
}
}
fn main() {
let cache = Cache::new();
// Many concurrent reads
let readers: Vec<_> = (0..4)
.map(|i| {
let cache = &cache;
std::thread::spawn(move || {
for j in 0..1000 {
let key = format!("key-{}", j % 10);
let _ = cache.get(&key);
}
})
})
.collect();
// Occasional writes
let writer = std::thread::spawn(|| {
for i in 0..100 {
let key = format!("key-{}", i % 10);
cache.insert(key, i);
}
});
for r in readers { r.join().unwrap(); }
writer.join().unwrap();
}parking_lot::RwLock excels in read-heavy caches with upgradable reads for check-then-modify patterns.
Real-World Example: Shared Configuration
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
// Configuration that changes rarely but is read frequently
#[derive(Clone)]
struct Config {
max_connections: usize,
timeout_ms: u64,
endpoint: String,
}
struct ConfigManagerStd {
config: StdRwLock<Config>,
}
impl ConfigManagerStd {
fn get_config(&self) -> std::sync::RwLockReadGuard<'_, Config> {
self.config.read().unwrap()
}
fn update_config(&self, new_config: Config) {
let mut guard = self.config.write().unwrap();
*guard = new_config;
}
}
struct ConfigManagerPl {
config: PlRwLock<Config>,
}
impl ConfigManagerPl {
fn get_config(&self) -> parking_lot::RwLockReadGuard<'_, Config> {
self.config.read()
}
fn update_config(&self, new_config: Config) {
let mut guard = self.config.write();
*guard = new_config;
}
}
// For read-heavy config access:
// - std works fine (reads don't block other reads)
// - parking_lot provides consistent behavior and simpler API (no unwrap)
// - parking_lot's fairness ensures config updates happen eventuallyConfiguration is typically read frequently and updated rarely, making RwLock ideal.
Synthesis
Key trade-offs:
| Aspect | std::sync::RwLock |
parking_lot::RwLock |
|---|---|---|
| Writer fairness | May starve writers (Linux) | Fair scheduling |
| Reader throughput | Higher (writers wait) | Slightly lower (fair queue) |
| Memory footprint | Larger, platform-dependent | Smaller, consistent |
| Poisoning | Yes | No |
| Upgradable reads | No | Yes |
| Cross-platform behavior | Varies | Consistent |
| Dependencies | None (std) | External crate |
| Uncontended speed | Good | Better |
| Try_lock returns | Result |
Option |
When to prefer parking_lot::RwLock:
| Scenario | Reason |
|---|---|
| Upgradable reads needed | Exclusive feature |
| Cross-platform consistency | Predictable behavior |
| Writer fairness matters | Fair queue prevents starvation |
| Memory-sensitive | Smaller footprint |
| Simpler error handling | No poisoning |
| High-contention with writes | Better contention handling |
When to prefer std::sync::RwLock:
| Scenario | Reason |
|---|---|
| No external dependencies | Standard library only |
| Poisoning semantics needed | Crash recovery handling |
| Read-only workloads | Maximum read throughput |
| Platform-specific behavior needed | Leverage native implementation |
Key insight: The primary trade-off between parking_lot::RwLock and std::sync::RwLock centers on fairness versus raw read throughput. std::sync::RwLock on Unix systems with pthreads allows new readers to acquire the lock even when a writer is waiting, maximizing read throughput but potentially starving writers indefinitely in read-heavy workloads. parking_lot::RwLock implements a fair queue that ensures writers eventually acquire the lock, at the cost of slightly reduced read throughput. Beyond fairness, parking_lot::RwLock offers practical advantages: upgradable reads (check-then-modify without releasing the lock), no lock poisoning (simpler error handling), smaller memory footprint, and consistent cross-platform behavior. For read-heavy workloads where writes must happen reliably—like caches that need occasional updates—parking_lot::RwLock's fairness guarantees and upgradable reads make it the better choice despite the dependency overhead. For pure read-mostly workloads where writes are rare and timing-insensitive, std::sync::RwLock's simpler dependency profile and reader-biased scheduling may suffice.
