How does parking_lot::RwLock differ from std::sync::RwLock in terms of fairness and poisoning behavior?

parking_lot::RwLock provides fair locking by default and does not implement poisoning, while std::sync::RwLock may exhibit writer starvation and uses poisoning to handle lock acquisition after a panic. Fairness in parking_lot ensures that a waiting writer will not be indefinitely blocked by a continuous stream of new readers. The absence of poisoning in parking_lot means lock acquisition always succeeds, whereas std::sync::RwLock returns a LockResult that must be handled in case a previous holder panicked. These design differences reflect different trade-offs between safety guarantees and ergonomics.

Basic Locking API Comparison

use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
 
fn basic_usage() {
    // std::sync::RwLock
    let std_lock = StdRwLock::new(42);
    
    // Read lock returns Result (poisoning)
    let std_read = std_lock.read().unwrap();
    println!("std read: {}", *std_read);
    drop(std_read);
    
    // Write lock returns Result (poisoning)
    let mut std_write = std_lock.write().unwrap();
    *std_write += 1;
    drop(std_write);
    
    // parking_lot::RwLock
    let pl_lock = PlRwLock::new(42);
    
    // Read lock returns guard directly (no poisoning)
    let pl_read = pl_lock.read();
    println!("parking_lot read: {}", *pl_read);
    drop(pl_read);
    
    // Write lock returns guard directly (no poisoning)
    let mut pl_write = pl_lock.write();
    *pl_write += 1;
    drop(pl_write);
}

The key API difference: parking_lot returns guards directly; std returns LockResult.

Fairness: Writer Starvation

use std::sync::RwLock as StdRwLock;
use std::thread;
use std::time::Duration;
 
fn std_rwlock_writer_starvation() {
    let lock = StdRwLock::new(0);
    
    // In std::sync::RwLock, continuous readers can starve writers
    // The lock is NOT necessarily fair
    
    // Thread 1: holds read lock
    let lock1 = lock.clone();
    let h1 = thread::spawn(move || {
        let _read = lock1.read().unwrap();
        thread::sleep(Duration::from_millis(100));
    });
    
    // Thread 2: wants write lock (may wait indefinitely)
    let lock2 = lock.clone();
    let h2 = thread::spawn(move || {
        thread::sleep(Duration::from_millis(10));
        let _write = lock2.write().unwrap();  // May starve
    });
    
    // Thread 3: new reader (may acquire before writer)
    let lock3 = lock.clone();
    let h3 = thread::spawn(move || {
        thread::sleep(Duration::from_millis(20));
        let _read = lock3.read().unwrap();  // Could skip ahead of writer
    });
    
    // std::sync::RwLock does NOT guarantee writer fairness
    // Writers can be starved by continuous stream of readers
}
 
use parking_lot::RwLock as PlRwLock;
 
fn parking_lot_fairness() {
    let lock = PlRwLock::new(0);
    
    // parking_lot::RwLock is fair by default
    // When a writer is waiting, new readers block
    
    // This prevents writer starvation
    
    let lock1 = lock.clone();
    let h1 = thread::spawn(move || {
        let _read = lock1.read();
        thread::sleep(Duration::from_millis(100));
    });
    
    let lock2 = lock.clone();
    let h2 = thread::spawn(move || || {
        thread::sleep(Duration::from_millis(10));
        // When this writer arrives and waits...
        let _write = lock2.write();
        // ...new readers will block until writer completes
    });
    
    let lock3 = lock.clone();
    let h3 = thread::spawn(move || {
        thread::sleep(Duration::from_millis(20));
        // This reader will block if writer is waiting
        let _read = lock3.read();
    });
}

parking_lot guarantees writers get fair access; std does not.

Fairness Mechanism Details

use parking_lot::RwLock;
 
fn fairness_explanation() {
    // parking_lot fairness algorithm:
    //
    // 1. When a writer is waiting, new readers block
    // 2. Existing readers can still complete
    // 3. Writer gets lock after all current readers release
    // 4. New readers queue up behind the writer
    //
    // This prevents writer starvation
    
    let lock = RwLock::new(vec![]);
    
    // Scenario:
    // - Reader A holds lock
    // - Writer W requests lock (waits)
    // - Reader B requests lock (blocks, waits behind writer)
    // - Reader A releases
    // - Writer W gets lock
    // - Writer W releases
    // - Reader B gets lock
    
    // std::sync::RwLock behavior varies by platform:
    // - May allow Reader B to acquire before Writer W
    // - Continuous readers can indefinitely delay writers
}

parking_lot implements explicit fairness; std behavior is platform-dependent.

Poisoning: std::sync::RwLock

use std::sync::RwLock;
use std::thread;
 
fn std_poisoning() {
    let lock = RwLock::new(42);
    
    // Thread panics while holding write lock
    let lock_clone = lock.clone();
    let handle = thread::spawn(move || {
        let mut guard = lock_clone.write().unwrap();
        *guard = 100;
        panic!("Something went wrong!");
    });
    
    handle.join().unwrap_err();
    
    // Lock is now "poisoned"
    // Attempting to acquire returns Err(PoisonError)
    let read_result = lock.read();
    match read_result {
        Ok(guard) => println!("Got lock: {}", *guard),
        Err(poison_err) => {
            // Lock is poisoned, but we can still access data
            let guard = poison_err.into_inner();
            println!("Lock was poisoned, value: {}", *guard);  // 100
            
            // The write completed before panic
            // But lock is marked as potentially inconsistent
        }
    }
    
    // Or use unwrap to panic on poison
    // let guard = lock.read().unwrap();  // Would panic!
}

std::sync::RwLock poisoning marks the lock as compromised after a panic.

No Poisoning: parking_lot::RwLock

use parking_lot::RwLock;
use std::thread;
 
fn parking_lot_no_poisoning() {
    let lock = RwLock::new(42);
    
    // Thread panics while holding write lock
    let lock_clone = lock.clone();
    let handle = thread::spawn(move || {
        let mut guard = lock_clone.write();
        *guard = 100;
        panic!("Something went wrong!");
        // Guard is released during unwinding
    });
    
    handle.join().unwrap_err();
    
    // Lock acquisition just works - no poisoning
    let mut guard = lock.write();
    println!("Value: {}", *guard);  // 100
    
    // No Result to unwrap, no poisoning to handle
    // Lock is always available
}

parking_lot::RwLock never poisons; lock acquisition always succeeds.

Handling Poisoned Locks in std

use std::sync::RwLock;
 
fn recover_from_poison() {
    let lock = RwLock::new(vec![1, 2, 3]);
    
    // Simulate panic while holding lock
    let lock_clone = lock.clone();
    std::panic::catch_unwind(|| {
        let mut guard = lock_clone.write().unwrap();
        guard.push(4);
        panic!("oops");
    }).unwrap_err();
    
    // Option 1: Recover and continue
    let guard = lock.read().unwrap_or_else(|e| {
        println!("Lock was poisoned, recovering...");
        e.into_inner()  // Get the guard anyway
    });
    println!("Data: {:?}", *guard);  // [1, 2, 3, 4]
    
    // Option 2: Clear poison and continue
    lock.clear_poison();
    let guard = lock.read().unwrap();  // Now Ok
    println!("After clear: {:?}", *guard);
    
    // Option 3: Use recover methods
    let guard = lock.write().recover(|e| {
        println!("Recovering from poison");
        e.into_inner()
    });
}

std::sync::RwLock provides mechanisms to recover from poisoned state.

Design Philosophy Differences

// std::sync::RwLock philosophy:
// - Safety first: assume data may be inconsistent after panic
// - Force explicit handling of potential inconsistency
// - Poisoning signals: "a panic occurred while holding this"
// - Trade-off: more boilerplate, stronger guarantees
 
// parking_lot::RwLock philosophy:
// - Pragmatic: panics don't necessarily corrupt data
// - Lock state is always recoverable
// - Simpler API: no Results, no poison handling
// - Trade-off: caller must ensure consistency if needed
 
fn philosophy_comparison() {
    // Question: Is your data actually corrupted by a panic?
    //
    // If the panic occurred:
    // - After a complete write: data is consistent
    // - Mid-update: data might be inconsistent
    // - During read: data is consistent
    //
    // std::sync::RwLock: Assumes worst case, always signals
    // parking_lot::RwLock: Assumes caller handles their own invariants
}

The difference reflects different safety philosophies: explicit vs implicit.

Read-Write Lock Patterns

use parking_lot::RwLock;
use std::thread;
 
fn reader_writer_pattern() {
    let data = RwLock::new(vec![]);
    
    // Multiple readers can hold lock simultaneously
    let handles: Vec<_> = (0..5)
        .map(|i| {
            let data = data.clone();
            thread::spawn(move || {
                let read = data.read();
                println!("Reader {} sees {} items", i, read.len());
            })
        })
        .collect();
    
    for h in handles {
        h.join().unwrap();
    }
    
    // Only one writer at a time
    let writer = {
        let data = data.clone();
        thread::spawn(move || {
            let mut write = data.write();
            write.push(42);
            println!("Writer added item");
        })
    };
    
    writer.join().unwrap();
    
    // Upgradable read lock (parking_lot feature)
    let data = RwLock::new(HashMap::new());
    {
        let read = data.upgradable_read();
        if !read.contains_key("key") {
            let mut write = RwLockUpgradableReadGuard::upgrade(read);
            write.insert("key".to_string(), 42);
        }
    }
}

parking_lot provides additional lock types like upgradable reads.

Upgradable Read Locks

use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 
fn upgradable_read() {
    let lock = RwLock::new(HashMap::<String, i32>::new());
    
    // parking_lot supports upgradable read locks
    // Can read, then conditionally upgrade to write
    
    {
        let read = lock.upgradable_read();
        
        if !read.contains_key("key") {
            // Upgrade to write lock
            let mut write = RwLockUpgradableReadGuard::upgrade(read);
            write.insert("key".to_string(), 42);
        }
        // If not upgraded, read guard is dropped normally
    }
    
    // std::sync::RwLock does NOT have upgradable reads
    // You would need to:
    // 1. Drop read lock
    // 2. Acquire write lock
    // 3. Re-check condition (another thread may have modified)
}

parking_lot provides upgradable reads which std does not have.

Performance Characteristics

use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
 
fn performance_comparison() {
    // parking_lot::RwLock advantages:
    // - Smaller memory footprint
    // - Faster uncontended acquisition
    // - No poisoning overhead
    // - Better fairness without extra cost
    // - More efficient under contention
    
    // std::sync::RwLock:
    // - Uses OS primitives (pthread_rwlock on Unix)
    // - Larger memory footprint
    // - System call overhead on some platforms
    // - Poisoning tracking overhead
    // - Platform-dependent fairness
    
    // Benchmark conceptually:
    let std_lock = StdRwLock::new(0u64);
    let pl_lock = PlRwLock::new(0u64);
    
    // Uncontended read acquisition:
    // parking_lot: ~5-10ns
    // std: ~10-20ns (varies by platform)
    
    // Uncontended write acquisition:
    // parking_lot: ~5-10ns
    // std: ~10-20ns
    
    // Under contention, parking_lot's fairness can reduce
    // worst-case latency for writers
}

parking_lot generally offers better performance due to its design.

When Poisoning Matters

use std::sync::RwLock;
 
struct Bank {
    accounts: RwLock<HashMap<String, u64>>,
}
 
impl Bank {
    fn transfer(&self, from: &str, to: &str, amount: u64) -> Result<(), String> {
        let mut accounts = self.accounts.write().unwrap();
        
        // Complex operation that might panic
        let from_balance = accounts.get(from).ok_or("Account not found")?;
        if *from_balance < amount {
            return Err("Insufficient funds".into());
        }
        
        // If we panic here, lock is poisoned
        *accounts.get_mut(from).unwrap() -= amount;
        *accounts.get_mut(to).unwrap() += amount;
        
        Ok(())
    }
    
    fn check_accounts(&self) -> Result<(), String> {
        // With std::sync::RwLock, we know if a transfer panicked
        match self.accounts.read() {
            Ok(accounts) => {
                println!("Accounts: {:?}", *accounts);
                Ok(())
            }
            Err(poison) => {
                // A panic occurred during a transfer
                // The data might be inconsistent!
                // - from_balance might be decremented
                // - to_balance might NOT be incremented
                Err("Accounts may be inconsistent after panic".into())
            }
        }
    }
}

Poisoning signals potential data inconsistency from interrupted operations.

When Poisoning Doesn't Matter

use parking_lot::RwLock;
 
struct Cache {
    data: RwLock<HashMap<String, String>>,
}
 
impl Cache {
    fn get_or_insert(&self, key: &str, compute: impl FnOnce() -> String) -> String {
        // Check if already cached
        {
            let read = self.data.read();
            if let Some(value) = read.get(key) {
                return value.clone();
            }
        }
        
        // Compute and insert
        let mut write = self.data.write();
        
        // Double-check (another thread may have inserted)
        if let Some(value) = write.get(key) {
            return value.clone();
        }
        
        // compute() might panic - but that's fine
        // The lock is released, cache is still valid
        let value = compute();
        write.insert(key.to_string(), value.clone());
        
        value
    }
    
    // With parking_lot, no poisoning means:
    // - Cache is always accessible after panic
    // - No need to handle poison errors
    // - Panicked computation just didn't complete
}

For caches and similar use cases, poisoning adds unnecessary complexity.

Migration Between Implementations

// Migrating from std to parking_lot
 
// Before (std::sync::RwLock):
use std::sync::RwLock;
 
fn with_std() {
    let lock = RwLock::new(42);
    let guard = lock.read().unwrap();  // Handle Result
}
 
// After (parking_lot::RwLock):
use parking_lot::RwLock;
 
fn with_parking_lot() {
    let lock = RwLock::new(42);
    let guard = lock.read();  // Direct guard, no Result
}
 
// Key changes:
// 1. Remove .unwrap() or match on lock acquisition
// 2. Remove poison recovery code
// 3. Take advantage of upgradable reads if useful
// 4. Rely on fairness guarantees
 
// Migrating from parking_lot to std:
 
// Before (parking_lot):
use parking_lot::RwLock;
 
fn with_pl() {
    let lock = RwLock::new(42);
    let guard = lock.write();
    *guard = 100;
}
 
// After (std::sync::RwLock):
use std::sync::RwLock;
 
fn with_std() {
    let lock = RwLock::new(42);
    let mut guard = lock.write().unwrap();  // Add Result handling
    *guard = 100;
}
 
// Handle potential writer starvation with std
// Consider using std::sync::RwLock with careful design

Migration is straightforward but requires handling the API differences.

Comparison Summary

Feature std::sync::RwLock parking_lot::RwLock
Fairness Not guaranteed Guaranteed (default)
Writer starvation Possible Prevented
Poisoning Yes No
Lock acquisition returns LockResult<Guard> Guard directly
Upgradable read No Yes
Memory overhead Larger Smaller
Implementation OS primitives Custom parking
Platform behavior Varies Consistent

Synthesis

The fairness and poisoning differences between std::sync::RwLock and parking_lot::RwLock reflect different design philosophies:

Fairness:

  • parking_lot::RwLock guarantees fairness: waiting writers will not be starved by continuous readers
  • std::sync::RwLock does not guarantee fairness; behavior varies by platform
  • Fairness matters when writer latency is important and reader load is high

Poisoning:

  • std::sync::RwLock poisons after a panic while locked; forces explicit handling
  • parking_lot::RwLock never poisons; simpler API but no signal of interrupted operations
  • Poisoning is valuable when operations must be atomic and interrupted state matters

Choose std::sync::RwLock when:

  • You need poisoning to detect potential inconsistency
  • Working in environments where parking_lot isn't available
  • Platform-native locks are preferred

Choose parking_lot::RwLock when:

  • Writer fairness matters (prevent starvation)
  • Simpler API without poisoning is desirable
  • Better performance is needed
  • Upgradable read locks are useful
  • Consistent cross-platform behavior is required

Key insight: The poisoning mechanism in std::sync::RwLock is a safety feature that signals potential data corruption, but it adds complexity for cases where panics don't actually corrupt state. parking_lot::RwLock opts for simplicity and performance, assuming that if you need consistency guarantees, you'll implement them at a higher level. The fairness guarantee in parking_lot is often more valuable in practice than the poisoning guarantee in std, especially in read-heavy workloads where writers need timely access.