Implementing a Read-Write Lock (RwLock) in Rust
Concurrent access to shared mutable data is a fundamental challenge in multithreaded programming. While exclusive locks prevent any concurrent access, read-write locks offer a more optimized solution by allowing multiple readers to access data simultaneously, while still ensuring exclusive access for writers. This challenge asks you to implement a basic Read-Write Lock mechanism in Rust.
Problem Description
Your task is to create a Rust RwLock struct that manages access to a shared data payload. This RwLock should support two types of operations: acquiring a read lock and acquiring a write lock.
Key Requirements:
- Shared Data: The
RwLockshould be able to hold any type of data (T). - Reader Access: Multiple threads should be able to acquire a read lock concurrently. When a read lock is held, no write lock can be acquired.
- Writer Access: Only one thread can acquire a write lock at any given time. When a write lock is held, no other read or write locks can be acquired.
- Fairness (Optional but Recommended): While not strictly enforced for a basic implementation, consider how to prevent writer starvation (where a continuous stream of readers prevents a waiting writer from ever acquiring the lock). A simple approach would be to prioritize writers once they've signaled their intent to acquire the lock.
- Lock Guards: The lock acquisition methods should return a guard object (similar to
std::sync::MutexGuardorstd::sync::RwLockReadGuard/WriteGuard). These guards should automatically release the lock when they go out of scope (RAII pattern).
Expected Behavior:
- If a thread requests a read lock and no write lock is held, it should be granted immediately.
- If a thread requests a read lock and a write lock is held, it must block until the write lock is released.
- If a thread requests a write lock and no read or write locks are held, it should be granted immediately.
- If a thread requests a write lock and any read or write locks are held, it must block until all existing locks are released.
Edge Cases to Consider:
- Multiple threads attempting to acquire read locks simultaneously.
- Multiple threads attempting to acquire write locks simultaneously.
- A mix of read and write lock requests.
- Threads acquiring and releasing locks repeatedly.
Examples
Example 1: Basic Read Operations
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let data = Arc::new(RwLock::new(10));
let mut handles = vec![];
for i in 0..5 {
let data_clone = Arc::clone(&data);
handles.push(thread::spawn(move || {
let reader = data_clone.read().unwrap();
println!("Reader {} got lock, value: {}", i, *reader);
// Lock is automatically released when `reader` goes out of scope
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("All readers finished.");
}
Output: (Order of readers may vary)
Reader 0 got lock, value: 10
Reader 1 got lock, value: 10
Reader 2 got lock, value: 10
Reader 3 got lock, value: 10
Reader 4 got lock, value: 10
All readers finished.
Explanation: Five threads acquire read locks. Since no write lock is held, they all acquire the lock concurrently and read the value 10. The output order might differ due to thread scheduling.
Example 2: Writer Blocking Readers
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
let data = Arc::new(RwLock::new(10));
let mut handles = vec![];
// A writer thread
let data_writer = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("Writer attempting to acquire lock...");
let mut writer = data_writer.write().unwrap();
println!("Writer acquired lock. Modifying value...");
*writer += 5;
thread::sleep(Duration::from_millis(100)); // Hold lock for a bit
println!("Writer releasing lock.");
// Lock is released here
}));
// Reader threads
thread::sleep(Duration::from_millis(10)); // Ensure writer likely starts first
for i in 0..3 {
let data_clone = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("Reader {} attempting to acquire lock...", i);
let reader = data_clone.read().unwrap();
println!("Reader {} acquired lock, value: {}", i, *reader);
// Lock is released here
}));
}
for handle in handles {
handle.join().unwrap();
}
let final_value = data.read().unwrap();
println!("Final value: {}", *final_value);
}
Output: (Order of reader attempts may vary, but writer should output its messages before readers acquire locks)
Writer attempting to acquire lock...
Writer acquired lock. Modifying value...
Reader 0 attempting to acquire lock...
Reader 1 attempting to acquire lock...
Reader 2 attempting to acquire lock...
Writer releasing lock.
Reader 0 acquired lock, value: 15
Reader 1 acquired lock, value: 15
Reader 2 acquired lock, value: 15
Final value: 15
Explanation: The writer thread attempts to acquire the lock. While it holds the lock, the reader threads (started shortly after) will block. Once the writer releases its lock, the readers can acquire their locks and see the updated value.
Example 3: Writer Blocking Writers
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
fn main() {
let data = Arc::new(RwLock::new(0));
let mut handles = vec![];
// First writer
let data1 = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("Writer 1 attempting to acquire lock...");
let mut writer = data1.write().unwrap();
println!("Writer 1 acquired lock. Modifying value...");
*writer = 1;
thread::sleep(Duration::from_millis(200)); // Hold lock
println!("Writer 1 releasing lock.");
}));
// Second writer (will block)
let data2 = Arc::clone(&data);
handles.push(thread::spawn(move || {
thread::sleep(Duration::from_millis(50)); // Give Writer 1 a head start
println!("Writer 2 attempting to acquire lock...");
let mut writer = data2.write().unwrap();
println!("Writer 2 acquired lock. Modifying value...");
*writer = 2;
println!("Writer 2 releasing lock.");
}));
for handle in handles {
handle.join().unwrap();
}
let final_value = data.read().unwrap();
println!("Final value: {}", *final_value);
}
Output:
Writer 1 attempting to acquire lock...
Writer 1 acquired lock. Modifying value...
Writer 2 attempting to acquire lock...
Writer 1 releasing lock.
Writer 2 acquired lock. Modifying value...
Writer 2 releasing lock.
Final value: 2
Explanation: Writer 1 acquires the lock. Writer 2 attempts to acquire it but must wait. Writer 1 releases, then Writer 2 acquires and modifies the data.
Constraints
- Your implementation should not use
std::sync::RwLockor any other existing Rust concurrency primitives that directly provideRwLockfunctionality. - You will likely need to use
std::sync::Mutexfor internal synchronization primitives (e.g., to protect the lock state itself). - You will need to use
std::sync::Condvarto signal threads when they can proceed. - The
RwLockshould be generic over the typeTit protects. - The
readandwritemethods should returnResult<Guard, PoisonError<Guard>>whereGuardis your custom read or write guard type. Handle poisoning gracefully, though for this challenge, simply propagating thePoisonErroris sufficient.
Notes
- Think about the state variables you need to track: how many readers are active, is a writer active, are there writers waiting, are there readers waiting.
- Consider using
std::sync::atomictypes for counters if you want to explore a more lock-free approach for certain state variables, butMutexis perfectly acceptable for this challenge. - The RAII pattern for your guards is crucial. Ensure that
Dropimplementations correctly release the locks and potentially notify waiting threads. - You'll need to implement
ReadGuardandWriteGuardstructs. These guards should dereference to the protected data (&TforReadGuard,&mut TforWriteGuard).