How does parking_lot::RwLock::upgradable_read differ from a regular read lock and when is it useful?

parking_lot::RwLock::upgradable_read provides a read lock that can be atomically upgraded to a write lock without releasing the lock first, solving a common deadlock scenario that occurs when you need to read data, make a decision, and then write based on that decision. A regular read lock must be released before acquiring a write lock, creating a window where another thread could modify the data between your read and write operations. The upgradable read lock ensures exclusive access during the upgrade decision, preventing other writers from acquiring the lock while you decide whether to upgrade. This pattern is valuable for read-modify-write operations, conditional updates, and any scenario where you need to read first and then potentially write based on the read result.

Regular Read Lock Behavior

use std::sync::RwLock;
 
fn main() {
    let lock = RwLock::new(0);
    
    // Regular read lock allows multiple readers
    let read1 = lock.read().unwrap();
    let read2 = lock.read().unwrap(); // OK: multiple readers
    
    // Cannot write while readers hold locks
    // let write = lock.write().unwrap(); // Would block!
    
    drop(read1);
    drop(read2);
    
    // Now writing is possible
    let mut write = lock.write().unwrap();
    *write = 42;
}

Regular read locks allow concurrent readers but block writers.

The Read-Then-Write Deadlock Problem

use std::sync::RwLock;
use std::thread;
 
fn main() {
    let lock = RwLock::new(vec![1, 2, 3]);
    
    // PROBLEM: Need to read, then write if condition met
    // With std::sync::RwLock, this can deadlock
    
    // Approach 1: Release read, acquire write (BROKEN)
    {
        let data = lock.read().unwrap();
        if data.len() < 10 {
            drop(data); // Release read lock
            // PROBLEM: Another thread could modify here!
            let mut data = lock.write().unwrap();
            data.push(4); // Condition might no longer be true
        }
    }
    
    // Approach 2: Always acquire write (INEFFICIENT)
    {
        let mut data = lock.write().unwrap();
        // Only needed read access, but blocked all readers
        if data.len() < 10 {
            data.push(4);
        }
    }
}

The standard library's RwLock forces a choice between correctness and efficiency.

Upgradable Read Lock Basics

use parking_lot::RwLock;
 
fn main() {
    let lock = RwLock::new(vec![1, 2, 3]);
    
    // Upgradable read: can read now, upgrade to write later
    let upgradable = lock.upgradable_read();
    
    println!("Current data: {:?}", *upgradable);
    
    // Decision: need to write
    if upgradable.len() < 10 {
        // Atomically upgrade to write lock
        let mut write = RwLockUpgradableReadGuard::upgrade(upgradable);
        write.push(4);
        // write is now a write guard
    }
    // If we didn't upgrade, upgradable would release as read lock
}

upgradable_read provides a path to atomically become a write lock.

Importing the Upgrade Capability

use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 
fn main() {
    let lock = RwLock::new(0);
    
    // Upgradable read blocks new readers and writers
    let upgradable = lock.upgradable_read();
    
    // Read is allowed
    println!("Value: {}", *upgradable);
    
    // Upgrade to write
    let mut write = RwLockUpgradableReadGuard::upgrade(upgradable);
    *write = 42;
    println!("New value: {}", *write);
}

The RwLockUpgradableReadGuard::upgrade function performs the atomic upgrade.

Deadlock Prevention Pattern

use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use std::thread;
use std::sync::Arc;
 
fn main() {
    let lock = Arc::new(RwLock::new(HashMap::<String, i32>::new()));
    
    // Without upgradable: potential deadlock
    // Thread 1: read, then try to write
    // Thread 2: read, then try to write
    // Both hold read locks, both waiting for write lock = DEADLOCK
    
    // With upgradable: safe
    let lock1 = Arc::clone(&lock);
    let h1 = thread::spawn(move || {
        let upgradable = lock1.upgradable_read();
        if !upgradable.contains_key("counter") {
            let mut write = RwLockUpgradableReadGuard::upgrade(upgradable);
            write.insert("counter".to_string(), 0);
        }
    });
    
    let lock2 = Arc::clone(&lock);
    let h2 = thread::spawn(move || {
        let upgradable = lock2.upgradable_read();
        if let Some(counter) = upgradable.get_mut("counter") {
            // Already have upgradable, can safely upgrade
            let mut write = RwLockUpgradableReadGuard::upgrade(upgradable);
            *write.get_mut("counter").unwrap() += 1;
        }
    });
    
    h1.join().unwrap();
    h2.join().unwrap();
}

Upgradable reads prevent the read-then-write deadlock pattern.

Lock Compatibility Matrix

use parking_lot::RwLock;
 
fn main() {
    let lock = RwLock::new(0);
    
    // REGULAR READ LOCK COMPATIBILITY:
    // - Multiple read locks: COMPATIBLE
    // - Write lock: BLOCKS
    // - Upgradable read: BLOCKS (only one allowed)
    
    // UPGRADABLE READ LOCK COMPATIBILITY:
    // - Read locks: COMPATIBLE (new readers allowed)
    // - Write lock: BLOCKS
    // - Another upgradable read: BLOCKS (only one upgradable at a time)
    
    // WRITE LOCK COMPATIBILITY:
    // - Read locks: BLOCKS
    // - Upgradable read: BLOCKS
    // - Another write lock: BLOCKS
    
    // Demonstration
    {
        let _read1 = lock.read();
        let _read2 = lock.read(); // OK: multiple readers
        // let _upgradable = lock.upgradable_read(); // BLOCKS: would wait
    }
    
    {
        let _upgradable = lock.upgradable_read();
        // let _upgradable2 = lock.upgradable_read(); // BLOCKS: only one upgradable
        // let _write = lock.write(); // BLOCKS: would wait
        let _read = lock.read(); // OK: readers allowed during upgradable
    }
}

Only one upgradable read can exist at a time, blocking writers but allowing readers.

Conditional Update Pattern

use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
 
struct Cache {
    data: RwLock<HashMap<String, String>>,
}
 
impl Cache {
    fn new() -> Self {
        Cache {
            data: RwLock::new(HashMap::new()),
        }
    }
    
    fn get_or_insert(&self, key: &str, value: impl FnOnce() -> String) -> String {
        // First, try to get with regular read
        {
            let read = self.data.read();
            if let Some(v) = read.get(key) {
                return v.clone();
            }
        }
        
        // Not found, need upgradable to safely insert
        let upgradable = self.data.upgradable_read();
        
        // Double-check (another thread may have inserted)
        if let Some(v) = upgradable.get(key) {
            return v.clone();
        }
        
        // Compute and insert
        let computed = value();
        let mut write = RwLockUpgradableReadGuard::upgrade(upgradable);
        write.insert(key.to_string(), computed.clone());
        
        computed
    }
}
 
fn main() {
    let cache = Cache::new();
    
    let result = cache.get_or_insert("key", || {
        println!("Computing value...");
        "computed_value".to_string()
    });
    
    println!("Result: {}", result);
}

The classic "check-then-insert" pattern is safe with upgradable reads.

Downgrade from Write to Read

use parking_lot::{RwLock, RwLockWriteGuard};
 
fn main() {
    let lock = RwLock::new(vec![1, 2, 3]);
    
    // Start with write lock
    let mut write = lock.write();
    write.push(4);
    
    // Downgrade to read (keep access, allow other readers)
    let read = RwLockWriteGuard::downgrade(write);
    
    // Can still read
    println!("Data: {:?}", *read);
    
    // Other readers can now join
    let read2 = lock.read(); // Would succeed
    println!("Concurrent read: {:?}", *read2);
}

Write locks can downgrade to read, completing the flexibility spectrum.

Comparing Approaches

use parking_lot::RwLock;
use std::time::Instant;
 
fn main() {
    let lock = RwLock::new((0u64, 0u64)); // (reads, writes)
    let iterations = 100_000;
    
    // Approach 1: Always write lock (pessimistic)
    let start = Instant::now();
    for _ in 0..iterations {
        let mut write = lock.write();
        write.1 += 1; // Always increment write counter
    }
    let pessimistic_time = start.elapsed();
    
    // Reset
    *lock.write() = (0, 0);
    
    // Approach 2: Upgradable read (optimistic)
    let start = Instant::now();
    for i in 0..iterations {
        let upgradable = lock.upgradable_read();
        
        // Only write conditionally (every other iteration)
        if i % 2 == 0 {
            let mut write = 
                parking_lot::RwLockUpgradableReadGuard::upgrade(upgradable);
            write.1 += 1;
        } else {
            // Just read, no upgrade needed
            upgradable.0 += 1; // Can't modify with upgradable
            // Actually upgradable doesn't allow mutation
            // Let's just drop it
        }
    }
    let optimistic_time = start.elapsed();
    
    println!("Pessimistic (always write): {:?}", pessimistic_time);
    println!("Optimistic (upgradable): {:?}", optimistic_time);
}

Upgradable reads are efficient when writes are rare but possible.

Thread Contention Scenarios

use parking_lot::RwLock;
use std::sync::Arc;
use std::thread;
 
fn main() {
    let lock = Arc::new(RwLock::new(0));
    
    // Scenario: Many readers, rare writer
    let mut handles = vec![];
    
    // Writers
    for i in 0..2 {
        let lock = Arc::clone(&lock);
        handles.push(thread::spawn(move || {
            for _ in 0..100 {
                // Upgradable: check then maybe write
                let upgradable = lock.upgradable_read();
                if *upgradable < 1000 {
                    let mut write = 
                        parking_lot::RwLockUpgradableReadGuard::upgrade(upgradable);
                    *write += 1;
                }
            }
        }));
    }
    
    // Readers
    for _ in 0..10 {
        let lock = Arc::clone(&lock);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                let read = lock.read();
                let _ = *read;
            }
        }));
    }
    
    for h in handles {
        h.join().unwrap();
    }
    
    println!("Final value: {}", *lock.read());
}

Readers can proceed concurrently with an upgradable lock held.

When Regular Read Is Sufficient

use parking_lot::RwLock;
 
fn main() {
    let lock = RwLock::new(vec![1, 2, 3]);
    
    // Just reading: use regular read
    let read = lock.read();
    println!("Data: {:?}", *read);
    drop(read);
    
    // Just writing: use write
    let mut write = lock.write();
    write.push(4);
    drop(write);
    
    // Reading with intent to ALWAYS write: use write from start
    {
        let mut write = lock.write();
        let first = write.first().copied();
        write.push(5); // Definitely writing
    }
    
    // Reading with CONDITIONAL write: use upgradable
    {
        let upgradable = lock.upgradable_read();
        if upgradable.len() < 10 {
            // Only upgrade if condition met
            let mut write = 
                parking_lot::RwLockUpgradableReadGuard::upgrade(upgradable);
            write.push(6);
        }
        // If condition not met, just drop upgradable
    }
}

Use upgradable reads only when the write is conditional.

Practical Use Case: Lazy Initialization

use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use std::sync::Arc;
 
struct LazyValue<T> {
    initialized: RwLock<bool>,
    value: RwLock<Option<T>>,
    init: Box<dyn Fn() -> T + Send + Sync>,
}
 
impl<T: Clone + Send + Sync> LazyValue<T> {
    fn new(init: impl Fn() -> T + Send + Sync + 'static) -> Self {
        LazyValue {
            initialized: RwLock::new(false),
            value: RwLock::new(None),
            init: Box::new(init),
        }
    }
    
    fn get(&self) -> T {
        // Fast path: already initialized
        {
            let read = self.initialized.read();
            if *read {
                return self.value.read().as_ref().unwrap().clone();
            }
        }
        
        // Slow path: need to initialize
        let upgradable = self.initialized.upgradable_read();
        
        // Double-check (another thread may have initialized)
        if *upgradable {
            return self.value.read().as_ref().unwrap().clone();
        }
        
        // Initialize
        let value = (self.init)();
        
        // Upgrade to write for both locks
        let mut init_write = RwLockUpgradableReadGuard::upgrade(upgradable);
        let mut value_write = self.value.write();
        
        *value_write = Some(value.clone());
        *init_write = true;
        
        value
    }
}
 
fn main() {
    let lazy = LazyValue::new(|| {
        println!("Computing expensive value...");
        42
    });
    
    let v1 = lazy.get(); // Prints "Computing expensive value..."
    let v2 = lazy.get(); // Returns cached, no print
    
    println!("Values: {}, {}", v1, v2);
}

Upgradable reads enable safe lazy initialization without always acquiring write locks.

Trade-offs Summary

// REGULAR READ (lock.read())
// Pros:
// - Multiple concurrent readers allowed
// - Fastest for read-only access
// - No special consideration needed
// Cons:
// - Cannot upgrade to write
// - Must release and re-acquire for write (potential race)
 
// UPGRADABLE READ (lock.upgradable_read())
// Pros:
// - Atomic upgrade to write lock
// - No race condition between read and write
// - Prevents deadlock in read-then-write pattern
// Cons:
// - Only one upgradable at a time
// - Blocks writers from acquiring
// - Slightly more overhead than regular read
 
// WRITE (lock.write())
// Pros:
// - Exclusive access
// - Can downgrade to read
// Cons:
// - Blocks all other access
// - Unnecessary if only reading
 
// Use upgradable when:
// - Read is definitely needed
// - Write may be needed based on read
// - Cannot tolerate race between read and write

Choose the lock type based on access pattern and write probability.

Synthesis

Core distinction:

  • read(): Multiple concurrent readers, cannot upgrade to write
  • upgradable_read(): Single upgradable, can atomically upgrade to write
  • write(): Exclusive access, can downgrade to read

Key behaviors:

  • read allows other readers; upgradable_read allows readers but blocks other upgradable reads
  • upgradable_read blocks writers during the decision phase
  • RwLockUpgradableReadGuard::upgrade() converts upgradable to write atomically
  • RwLockWriteGuard::downgrade() converts write to read

When to use each:

  • Use read() for pure read operations that will never write
  • Use write() for operations that will definitely write
  • Use upgradable_read() for read-then-maybe-write patterns

Common patterns:

  • Lazy initialization (check if initialized, write if not)
  • Conditional updates (read value, write only if condition met)
  • Check-and-insert (verify key doesn't exist before inserting)

Key insight: upgradable_read solves a fundamental limitation in standard library RwLock where acquiring a write lock after releasing a read lock creates a race condition. By holding an upgradable read, you prevent other writers from modifying the data between your read and write, ensuring the condition you checked remains valid when you upgrade. This is essential for correctness in read-modify-write scenarios where the write decision depends on the current state. The trade-off is reduced concurrency (only one upgradable read at a time), but this is acceptable when writes are rare compared to reads.