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 eventually

Configuration 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.