Loading page…
Rust walkthroughs
Loading page…
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
// 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 writeChoose the lock type based on access pattern and write probability.
Core distinction:
read(): Multiple concurrent readers, cannot upgrade to writeupgradable_read(): Single upgradable, can atomically upgrade to writewrite(): Exclusive access, can downgrade to readKey behaviors:
read allows other readers; upgradable_read allows readers but blocks other upgradable readsupgradable_read blocks writers during the decision phaseRwLockUpgradableReadGuard::upgrade() converts upgradable to write atomicallyRwLockWriteGuard::downgrade() converts write to readWhen to use each:
read() for pure read operations that will never writewrite() for operations that will definitely writeupgradable_read() for read-then-maybe-write patternsCommon patterns:
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.