What is the difference between parking_lot::RwLock::read and upgradable_read for read locks that may need promotion?
parking_lot::RwLock::read returns a standard shared read guard that cannot be upgraded to a write lock, while upgradable_read returns a special guard that can atomically upgrade to an exclusive write lock—critical for avoiding deadlocks when a read operation might need to write based on what it reads. The upgradable_read guard exists in an intermediate state: exclusive against other upgradable readers but shares with regular readers, enabling safe lock promotion without releasing and reacquiring.
Basic RwLock::read Usage
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
fn basic_read() {
let lock = RwLock::new(42);
// Standard read lock - shared access
let read_guard = lock.read();
println!("Value: {}", *read_guard);
// Multiple threads can hold read locks simultaneously
drop(read_guard); // Release read lock
}read() provides shared access; multiple readers can hold the lock simultaneously.
Basic upgradable_read Usage
use parking_lot::RwLock;
fn basic_upgradable_read() {
let lock = RwLock::new(vec
![1, 2, 3])
;
// Upgradable read - can later upgrade to write
let upgradable = lock.upgradable_read();
println!("Current length: {}", upgradable.len());
// Upgrade to write lock
let mut write_guard = upgradable.upgrade();
write_guard.push(4);
// Write guard released automatically
}upgradable_read() returns a guard that can upgrade to a write lock atomically.
The Lock Upgrading Problem
use parking_lot::RwLock;
fn the_problem() {
let lock = RwLock::new(HashMap::new());
// Naive approach - potential deadlock!
// Step 1: Acquire read lock
let read_guard = lock.read();
// Step 2: Check if value exists
if !read_guard.contains_key("key") {
// Step 3: Need to write - but we have a read lock!
drop(read_guard); // Release read lock
// Step 4: Acquire write lock
let mut write_guard = lock.write();
// PROBLEM: Between releasing read and acquiring write,
// another thread may have inserted the value!
// This is the "time-of-check to time-of-use" (TOCTOU) problem
write_guard.insert("key".to_string(), 42);
}
}Releasing a read lock to acquire a write lock creates a race condition.
How upgradable_read Solves This
use parking_lot::RwLock;
use std::collections::HashMap;
fn upgradable_solution() {
let lock = RwLock::new(HashMap::new());
// Acquire upgradable read lock
let upgradable = lock.upgradable_read();
// Check if value exists (while holding upgradable)
if !upgradable.contains_key("key") {
// Atomically upgrade to write lock
let mut write_guard = upgradable.upgrade();
// No race condition - no other thread can acquire write lock
// during the upgrade
write_guard.insert("key".to_string(), 42);
}
// If key existed, upgradable guard drops without upgrading
}upgradable_read provides atomic upgrade from read to write.
Lock Compatibility Rules
use parking_lot::RwLock;
fn lock_compatibility() {
// Lock compatibility matrix:
//
// | Lock Type | Read | Write | Upgradable |
// |-------------------|------|-------|------------|
// | Read | Yes | No | Yes |
// | Write | No | No | No |
// | Upgradable Read | Yes | No | No |
// | Upgraded Write | No | No | No |
// Key insight: Only ONE upgradable_read can exist at a time
// This prevents deadlock during upgrade
// Multiple read locks can coexist with one upgradable_read
// But no write lock can coexist with upgradable_read
let lock = RwLock::new(0);
let read1 = lock.read(); // OK - read shares with upgradable
let upgradable = lock.upgradable_read(); // OK
let read2 = lock.read(); // OK - read shares with upgradable
// let upgradable2 = lock.upgradable_read(); // BLOCKS - exclusive
// let write = lock.write(); // BLOCKS - upgradable holds it
drop(read1);
drop(read2);
// Now upgrade
let write = upgradable.upgrade(); // OK - no other readers
}Upgradable reads are exclusive against other upgradable reads.
Concurrent Access Patterns
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
fn concurrent_access() {
let lock = Arc::new(RwLock::new(0));
let lock_clone = lock.clone();
// Thread 1: Regular read
let h1 = thread::spawn(move || {
let read = lock.read();
println!("Thread 1 reading: {}", *read);
});
// Thread 2: Upgradable read (can coexist with Thread 1's read)
let h2 = thread::spawn(move || {
let upgradable = lock_clone.upgradable_read();
println!("Thread 2 reading (upgradable): {}", *upgradable);
// Upgrade to write
let mut write = upgradable.upgrade();
*write += 1;
println!("Thread 2 writing");
});
h1.join().unwrap();
h2.join().unwrap();
}Regular reads share with upgradable reads until the upgrade.
Downgrading from Write to Read
use parking_lot::RwLock;
fn downgrade_pattern() {
let lock = RwLock::new(0);
// Acquire write lock
let mut write_guard = lock.write();
*write_guard = 42;
// Downgrade to upgradable read
let upgradable = RwLockWriteGuard::downgrade(write_guard);
// Can still read
println!("Value: {}", *upgradable);
// Further downgrade to regular read
let read_guard = RwLockUpgradableReadGuard::downgrade(upgradable);
println!("Value: {}", *read_guard);
}Write locks can be downgraded to upgradable or regular read locks.
Performance Characteristics
use parking_lot::RwLock;
use std::time::Instant;
fn performance_comparison() {
let lock = RwLock::new(0u64);
// Regular read: fastest, most concurrent
let start = Instant::now();
for _ in 0..1_000_000 {
let _read = lock.read();
}
println!("Regular read: {:?}", start.elapsed());
// Upgradable read: slightly slower, exclusive against other upgradable
let start = Instant::now();
for _ in 0..1_000_000 {
let _upgradable = lock.upgradable_read();
}
println!("Upgradable read: {:?}", start.elapsed());
// Upgradable read has more overhead than regular read because:
// 1. Must track upgradable state separately
// 2. Must block other upgradable readers
// 3. Must maintain upgrade path
// However, upgradable is much faster than:
// lock.read() -> check -> lock.write() (two lock acquisitions)
}Upgradable reads have slightly more overhead than regular reads.
Common Pattern: Cache Initialization
use parking_lot::RwLock;
use std::collections::HashMap;
struct Cache {
data: RwLock<HashMap<String, String>>,
}
impl Cache {
fn get_or_insert(&self, key: &str, value: impl FnOnce() -> String) -> String {
// First try regular read for fast path
{
let read = self.data.read();
if let Some(v) = read.get(key) {
return v.clone();
}
}
// Key doesn't exist, use upgradable for atomic check-and-insert
let upgradable = self.data.upgradable_read();
// Double-check (another thread may have inserted while we waited)
if let Some(v) = upgradable.get(key) {
return v.clone();
}
// Upgrade to write and insert
let mut write = upgradable.upgrade();
let value = value();
write.insert(key.to_string(), value.clone());
value
}
}This pattern avoids the double-acquisition overhead and race conditions.
When upgradable_read Blocks
use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
fn blocking_behavior() {
let lock = Arc::new(RwLock::new(0));
// Scenario 1: Write lock held
let lock1 = lock.clone();
let write_guard = lock.write();
let h = thread::spawn(move || {
// BLOCKS - write lock held
let _upgradable = lock1.upgradable_read();
});
drop(write_guard); // Now thread can proceed
// Scenario 2: Another upgradable held
let upgradable1 = lock.upgradable_read();
// let upgradable2 = lock.upgradable_read(); // BLOCKS
// Scenario 3: Write waiting
let lock2 = lock.clone();
let _upgradable = lock.upgradable_read();
let h2 = thread::spawn(move || {
let _write = lock2.write(); // Waits for upgradable to release or upgrade
});
// The upgradable can still upgrade
// But upgrade() will block until all regular readers release
}Upgradable reads have specific blocking semantics.
Upgrade Deadlock Prevention
use parking_lot::RwLock;
fn no_deadlock() {
let lock = RwLock::new(0);
// parking_lot prevents upgrade deadlocks by:
// 1. Only allowing one upgradable reader at a time
// 2. Making upgrade() block until all regular readers release
let upgradable = lock.upgradable_read();
// If upgrade() is called while regular readers exist:
// - upgradable waits for all regular readers to finish
// - No new readers are allowed during this wait
// - Once all regular readers release, upgrade completes
// This prevents the deadlock scenario:
// Thread 1: upgradable_read -> waiting to upgrade
// Thread 2: read -> blocked waiting for upgradable to release
// Thread 1: upgrade -> blocked waiting for Thread 2 to release
// DEADLOCK!
//
// parking_lot solves this by:
// - Not allowing new regular readers when upgradable wants to upgrade
// - This means Thread 2's read would block, not proceed
}The exclusive nature of upgradable reads prevents upgrade deadlocks.
API Differences
use parking_lot::RwLock;
use parking_lot::{RwLockReadGuard, RwLockUpgradableReadGuard, RwLockWriteGuard};
fn api_differences() {
let lock = RwLock::new(42);
// read() -> RwLockReadGuard
let read_guard: RwLockReadGuard<_> = lock.read();
// - Implements Deref
// - Can be cloned (cheap reference count)
// - Cannot upgrade
// upgradable_read() -> RwLockUpgradableReadGuard
let upgradable_guard: RwLockUpgradableReadGuard<_> = lock.upgradable_read();
// - Implements Deref
// - Cannot be cloned (exclusive)
// - Can upgrade to write
// - Can downgrade to read
// upgrade() -> RwLockWriteGuard
let write_guard: RwLockWriteGuard<_> = upgradable_guard.upgrade();
// - Implements DerefMut
// - Exclusive access
// - Can downgrade back to upgradable or read
}Each guard type has different capabilities.
Downgrade Methods
use parking_lot::RwLock;
fn downgrade_chain() {
let lock = RwLock::new(42);
// Write -> Upgradable -> Read
let write = lock.write();
// Write has exclusive access
let upgradable = RwLockWriteGuard::downgrade(write);
// Upgradable can share with readers but not with other upgradable
let read = RwLockUpgradableReadGuard::downgrade(upgradable);
// Read can share with all readers
// Why downgrade?
// - Avoid re-acquiring locks
// - Maintain fairness for waiting writers
// - Allow other readers sooner
}Downgrading releases exclusivity progressively.
Compare with std::sync::RwLock
use std::sync::RwLock as StdRwLock;
use parking_lot::RwLock as PlRwLock;
fn comparison() {
// std::sync::RwLock
// - No upgradable read
// - Must release read lock before acquiring write
// - Potential for deadlock in upgrade scenarios
let std_lock = StdRwLock::new(0);
{
let read = std_lock.read().unwrap();
// Cannot upgrade - must drop and re-acquire
drop(read);
let mut write = std_lock.write().unwrap();
// Race condition possible between drop and write
}
// parking_lot::RwLock
// - Has upgradable_read
// - Atomic upgrade
// - No race condition
let pl_lock = PlRwLock::new(0);
{
let upgradable = pl_lock.upgradable_read();
// Atomic upgrade - no race
let mut write = upgradable.upgrade();
}
}parking_lot adds upgradable reads which std lacks.
Fairness and Ordering
use parking_lot::RwLock;
fn fairness() {
let lock = RwLock::new(0);
// parking_lot RwLock is fair by default:
// - Writers are preferred over readers when fair
// - Upgradable readers are treated specially
// Upgradable read ordering:
// 1. If write lock held: block
// 2. If upgradable held: block (only one at a time)
// 3. Otherwise: acquire
// Upgrade ordering:
// 1. Wait for all current regular readers to release
// 2. Block new regular readers
// 3. Once all readers release: become write lock
// This ensures:
// - No deadlock between upgradable and readers
// - Fair access for waiting writers
}parking_lot ensures fair lock acquisition order.
Real-World Example: Lazy Initialization
use parking_lot::RwLock;
use std::collections::HashMap;
struct LazyCache<T> {
initialized: RwLock<bool>,
data: RwLock<Option<T>>,
}
impl<T: Clone> LazyCache<T> {
fn new() -> Self {
Self {
initialized: RwLock::new(false),
data: RwLock::new(None),
}
}
fn get_or_init<F: FnOnce() -> T>(&self, init: F) -> T {
// Fast path: check with regular read
{
let initialized = self.initialized.read();
if *initialized {
let data = self.data.read();
return data.clone().unwrap();
}
}
// Slow path: use upgradable for atomic check-and-set
let mut initialized = self.initialized.upgradable_read();
if !*initialized {
// Upgrade and initialize
let mut init_guard = initialized.upgrade();
*init_guard = true;
let mut data = self.data.write();
*data = Some(init());
return data.clone().unwrap();
}
let data = self.data.read();
data.clone().unwrap()
}
}Upgradable reads enable thread-safe lazy initialization without races.
Summary Table
use parking_lot::RwLock;
fn summary() {
// | Feature | read() | upgradable_read() |
// |------------------------|---------------|-------------------|
// | Multiple concurrent | Yes | No (exclusive) |
// | Can share with readers | Yes | Yes |
// | Can upgrade to write | No | Yes |
// | Can downgrade | No | Yes (to read) |
// | Performance | Fastest | Slightly slower |
// | Blocks write locks | Yes | Yes |
// | Blocked by writes | Yes | Yes |
// | Use case | Pure read | Conditional write |
}Choose based on whether you might need to write.
Synthesis
Quick reference:
use parking_lot::RwLock;
let lock = RwLock::new(HashMap::new());
// Use read() when you only need to read
let read = lock.read();
let _ = read.get("key");
drop(read);
// Use upgradable_read() when you might need to write
let upgradable = lock.upgradable_read();
if !upgradable.contains_key("key") {
// Atomically upgrade to write
let mut write = upgradable.upgrade();
write.insert("key".to_string(), 42);
}
// If key existed, upgradable guard drops without upgradeWhen to use each:
// Use read() when:
// - Pure read-only access
// - Maximum concurrency needed
// - No conditional writes based on read value
// Use upgradable_read() when:
// - Reading to decide whether to write
// - Atomic check-then-update semantics
// - Avoiding TOCTOU race conditions
// - Lazy initialization patternsKey insight: upgradable_read exists to solve a fundamental concurrency problem—safely upgrading from shared to exclusive access. The naive approach of releasing a read lock and acquiring a write lock creates a race condition (TOCTOU vulnerability), but upgradable_read provides an atomic upgrade path. The mechanism works by treating upgradable readers specially: only one can exist at a time (exclusive against other upgradable readers), but they share with regular readers. When upgrade() is called, the upgradable guard blocks until all regular readers release, then atomically converts to a write lock without allowing new readers to sneak in. This prevents deadlock (the classic "readers-writers problem" where waiting writers and upgrading readers can deadlock) by ensuring the upgradable reader has priority over new readers during the upgrade. The cost is slightly higher overhead and exclusive access for upgradable readers, but this is far cheaper than the alternative of write lock acquisition for all potentially-mutating operations. Use read() for pure read scenarios where you'll never write; use upgradable_read() when your read might conditionally lead to a write—especially for cache lookup-and-insert patterns, lazy initialization, and any check-then-act logic.
