Loading page…
Rust walkthroughs
Loading page…
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.
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.
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.
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.
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.
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.
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.
// 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.
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.
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.
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.
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.
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.
// 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 designMigration is straightforward but requires handling the API differences.
| 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 |
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 readersstd::sync::RwLock does not guarantee fairness; behavior varies by platformPoisoning:
std::sync::RwLock poisons after a panic while locked; forces explicit handlingparking_lot::RwLock never poisons; simpler API but no signal of interrupted operationsChoose std::sync::RwLock when:
Choose parking_lot::RwLock when:
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.