Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f385ac2

Browse files
committedNov 18, 2024
Auto merge of rust-lang#128219 - connortsui20:rwlock-downgrade, r=tgross35
Rwlock downgrade Tracking Issue: rust-lang#128203 This PR adds a `downgrade` method for `RwLock` / `RwLockWriteGuard` on all currently supported platforms. Outstanding questions: - [x] ~~Does the `futex.rs` change affect performance at all? It doesn't seem like it will but we can't be certain until we bench it...~~ - [x] ~~Should the SOLID platform implementation [be ported over](rust-lang#128219 (comment)) to the `queue.rs` implementation to allow it to support downgrades?~~
2 parents 1cd1dd7 + 70326e8 commit f385ac2

File tree

7 files changed

+662
-261
lines changed

7 files changed

+662
-261
lines changed
 

‎std/src/sync/rwlock.rs

+70-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ mod tests;
44
use crate::cell::UnsafeCell;
55
use crate::fmt;
66
use crate::marker::PhantomData;
7-
use crate::mem::ManuallyDrop;
7+
use crate::mem::{ManuallyDrop, forget};
88
use crate::ops::{Deref, DerefMut};
99
use crate::ptr::NonNull;
10-
use crate::sync::{LockResult, TryLockError, TryLockResult, poison};
10+
use crate::sync::{LockResult, PoisonError, TryLockError, TryLockResult, poison};
1111
use crate::sys::sync as sys;
1212

1313
/// A reader-writer lock
@@ -574,8 +574,12 @@ impl<T> From<T> for RwLock<T> {
574574

575575
impl<'rwlock, T: ?Sized> RwLockReadGuard<'rwlock, T> {
576576
/// Creates a new instance of `RwLockReadGuard<T>` from a `RwLock<T>`.
577-
// SAFETY: if and only if `lock.inner.read()` (or `lock.inner.try_read()`) has been
578-
// successfully called from the same thread before instantiating this object.
577+
///
578+
/// # Safety
579+
///
580+
/// This function is safe if and only if the same thread has successfully and safely called
581+
/// `lock.inner.read()`, `lock.inner.try_read()`, or `lock.inner.downgrade()` before
582+
/// instantiating this object.
579583
unsafe fn new(lock: &'rwlock RwLock<T>) -> LockResult<RwLockReadGuard<'rwlock, T>> {
580584
poison::map_result(lock.poison.borrow(), |()| RwLockReadGuard {
581585
data: unsafe { NonNull::new_unchecked(lock.data.get()) },
@@ -957,6 +961,68 @@ impl<'a, T: ?Sized> RwLockWriteGuard<'a, T> {
957961
None => Err(orig),
958962
}
959963
}
964+
965+
/// Downgrades a write-locked `RwLockWriteGuard` into a read-locked [`RwLockReadGuard`].
966+
///
967+
/// This method will atomically change the state of the [`RwLock`] from exclusive mode into
968+
/// shared mode. This means that it is impossible for a writing thread to get in between a
969+
/// thread calling `downgrade` and the same thread reading whatever it wrote while it had the
970+
/// [`RwLock`] in write mode.
971+
///
972+
/// Note that since we have the `RwLockWriteGuard`, we know that the [`RwLock`] is already
973+
/// locked for writing, so this method cannot fail.
974+
///
975+
/// # Example
976+
///
977+
/// ```
978+
/// #![feature(rwlock_downgrade)]
979+
/// use std::sync::{Arc, RwLock, RwLockWriteGuard};
980+
///
981+
/// // The inner value starts as 0.
982+
/// let rw = Arc::new(RwLock::new(0));
983+
///
984+
/// // Put the lock in write mode.
985+
/// let mut main_write_guard = rw.write().unwrap();
986+
///
987+
/// let evil = rw.clone();
988+
/// let handle = std::thread::spawn(move || {
989+
/// // This will not return until the main thread drops the `main_read_guard`.
990+
/// let mut evil_guard = evil.write().unwrap();
991+
///
992+
/// assert_eq!(*evil_guard, 1);
993+
/// *evil_guard = 2;
994+
/// });
995+
///
996+
/// // After spawning the writer thread, set the inner value to 1.
997+
/// *main_write_guard = 1;
998+
///
999+
/// // Atomically downgrade the write guard into a read guard.
1000+
/// let main_read_guard = RwLockWriteGuard::downgrade(main_write_guard);
1001+
///
1002+
/// // Since `downgrade` is atomic, the writer thread cannot have set the inner value to 2.
1003+
/// assert_eq!(*main_read_guard, 1, "`downgrade` was not atomic");
1004+
///
1005+
/// // Clean up everything now
1006+
/// drop(main_read_guard);
1007+
/// handle.join().unwrap();
1008+
///
1009+
/// let final_check = rw.read().unwrap();
1010+
/// assert_eq!(*final_check, 2);
1011+
/// ```
1012+
#[unstable(feature = "rwlock_downgrade", issue = "128203")]
1013+
pub fn downgrade(s: Self) -> RwLockReadGuard<'a, T> {
1014+
let lock = s.lock;
1015+
1016+
// We don't want to call the destructor since that calls `write_unlock`.
1017+
forget(s);
1018+
1019+
// SAFETY: We take ownership of a write guard, so we must already have the `RwLock` in write
1020+
// mode, satisfying the `downgrade` contract.
1021+
unsafe { lock.inner.downgrade() };
1022+
1023+
// SAFETY: We have just successfully called `downgrade`, so we fulfill the safety contract.
1024+
unsafe { RwLockReadGuard::new(lock).unwrap_or_else(PoisonError::into_inner) }
1025+
}
9601026
}
9611027

9621028
impl<'a, T: ?Sized> MappedRwLockWriteGuard<'a, T> {

‎std/src/sync/rwlock/tests.rs

+105
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,108 @@ fn panic_while_mapping_write_unlocked_poison() {
501501

502502
drop(lock);
503503
}
504+
505+
#[test]
506+
fn test_downgrade_basic() {
507+
let r = RwLock::new(());
508+
509+
let write_guard = r.write().unwrap();
510+
let _read_guard = RwLockWriteGuard::downgrade(write_guard);
511+
}
512+
513+
#[test]
514+
fn test_downgrade_observe() {
515+
// Taken from the test `test_rwlock_downgrade` from:
516+
// https://github.com/Amanieu/parking_lot/blob/master/src/rwlock.rs
517+
518+
const W: usize = 20;
519+
const N: usize = 100;
520+
521+
// This test spawns `W` writer threads, where each will increment a counter `N` times, ensuring
522+
// that the value they wrote has not changed after downgrading.
523+
524+
let rw = Arc::new(RwLock::new(0));
525+
526+
// Spawn the writers that will do `W * N` operations and checks.
527+
let handles: Vec<_> = (0..W)
528+
.map(|_| {
529+
let rw = rw.clone();
530+
thread::spawn(move || {
531+
for _ in 0..N {
532+
// Increment the counter.
533+
let mut write_guard = rw.write().unwrap();
534+
*write_guard += 1;
535+
let cur_val = *write_guard;
536+
537+
// Downgrade the lock to read mode, where the value protected cannot be modified.
538+
let read_guard = RwLockWriteGuard::downgrade(write_guard);
539+
assert_eq!(cur_val, *read_guard);
540+
}
541+
})
542+
})
543+
.collect();
544+
545+
for handle in handles {
546+
handle.join().unwrap();
547+
}
548+
549+
assert_eq!(*rw.read().unwrap(), W * N);
550+
}
551+
552+
#[test]
553+
fn test_downgrade_atomic() {
554+
const NEW_VALUE: i32 = -1;
555+
556+
// This test checks that `downgrade` is atomic, meaning as soon as a write lock has been
557+
// downgraded, the lock must be in read mode and no other threads can take the write lock to
558+
// modify the protected value.
559+
560+
// `W` is the number of evil writer threads.
561+
const W: usize = 20;
562+
let rwlock = Arc::new(RwLock::new(0));
563+
564+
// Spawns many evil writer threads that will try and write to the locked value before the
565+
// initial writer (who has the exclusive lock) can read after it downgrades.
566+
// If the `RwLock` behaves correctly, then the initial writer should read the value it wrote
567+
// itself as no other thread should be able to mutate the protected value.
568+
569+
// Put the lock in write mode, causing all future threads trying to access this go to sleep.
570+
let mut main_write_guard = rwlock.write().unwrap();
571+
572+
// Spawn all of the evil writer threads. They will each increment the protected value by 1.
573+
let handles: Vec<_> = (0..W)
574+
.map(|_| {
575+
let rwlock = rwlock.clone();
576+
thread::spawn(move || {
577+
// Will go to sleep since the main thread initially has the write lock.
578+
let mut evil_guard = rwlock.write().unwrap();
579+
*evil_guard += 1;
580+
})
581+
})
582+
.collect();
583+
584+
// Wait for a good amount of time so that evil threads go to sleep.
585+
// Note: this is not strictly necessary...
586+
let eternity = crate::time::Duration::from_millis(42);
587+
thread::sleep(eternity);
588+
589+
// Once everyone is asleep, set the value to `NEW_VALUE`.
590+
*main_write_guard = NEW_VALUE;
591+
592+
// Atomically downgrade the write guard into a read guard.
593+
let main_read_guard = RwLockWriteGuard::downgrade(main_write_guard);
594+
595+
// If the above is not atomic, then it would be possible for an evil thread to get in front of
596+
// this read and change the value to be non-negative.
597+
assert_eq!(*main_read_guard, NEW_VALUE, "`downgrade` was not atomic");
598+
599+
// Drop the main read guard and allow the evil writer threads to start incrementing.
600+
drop(main_read_guard);
601+
602+
for handle in handles {
603+
handle.join().unwrap();
604+
}
605+
606+
let final_check = rwlock.read().unwrap();
607+
assert_eq!(*final_check, W as i32 + NEW_VALUE);
608+
}

‎std/src/sys/sync/rwlock/futex.rs

+47-5
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct RwLock {
1818
const READ_LOCKED: Primitive = 1;
1919
const MASK: Primitive = (1 << 30) - 1;
2020
const WRITE_LOCKED: Primitive = MASK;
21+
const DOWNGRADE: Primitive = READ_LOCKED.wrapping_sub(WRITE_LOCKED); // READ_LOCKED - WRITE_LOCKED
2122
const MAX_READERS: Primitive = MASK - 1;
2223
const READERS_WAITING: Primitive = 1 << 30;
2324
const WRITERS_WAITING: Primitive = 1 << 31;
@@ -53,6 +54,24 @@ fn is_read_lockable(state: Primitive) -> bool {
5354
state & MASK < MAX_READERS && !has_readers_waiting(state) && !has_writers_waiting(state)
5455
}
5556

57+
#[inline]
58+
fn is_read_lockable_after_wakeup(state: Primitive) -> bool {
59+
// We make a special case for checking if we can read-lock _after_ a reader thread that went to
60+
// sleep has been woken up by a call to `downgrade`.
61+
//
62+
// `downgrade` will wake up all readers and place the lock in read mode. Thus, there should be
63+
// no readers waiting and the lock should be read-locked (not write-locked or unlocked).
64+
//
65+
// Note that we do not check if any writers are waiting. This is because a call to `downgrade`
66+
// implies that the caller wants other readers to read the value protected by the lock. If we
67+
// did not allow readers to acquire the lock before writers after a `downgrade`, then only the
68+
// original writer would be able to read the value, thus defeating the purpose of `downgrade`.
69+
state & MASK < MAX_READERS
70+
&& !has_readers_waiting(state)
71+
&& !is_write_locked(state)
72+
&& !is_unlocked(state)
73+
}
74+
5675
#[inline]
5776
fn has_reached_max_readers(state: Primitive) -> bool {
5877
state & MASK == MAX_READERS
@@ -84,6 +103,9 @@ impl RwLock {
84103
}
85104
}
86105

106+
/// # Safety
107+
///
108+
/// The `RwLock` must be read-locked (N readers) in order to call this.
87109
#[inline]
88110
pub unsafe fn read_unlock(&self) {
89111
let state = self.state.fetch_sub(READ_LOCKED, Release) - READ_LOCKED;
@@ -100,11 +122,13 @@ impl RwLock {
100122

101123
#[cold]
102124
fn read_contended(&self) {
125+
let mut has_slept = false;
103126
let mut state = self.spin_read();
104127

105128
loop {
106-
// If we can lock it, lock it.
107-
if is_read_lockable(state) {
129+
// If we have just been woken up, first check for a `downgrade` call.
130+
// Otherwise, if we can read-lock it, lock it.
131+
if (has_slept && is_read_lockable_after_wakeup(state)) || is_read_lockable(state) {
108132
match self.state.compare_exchange_weak(state, state + READ_LOCKED, Acquire, Relaxed)
109133
{
110134
Ok(_) => return, // Locked!
@@ -116,9 +140,7 @@ impl RwLock {
116140
}
117141

118142
// Check for overflow.
119-
if has_reached_max_readers(state) {
120-
panic!("too many active read locks on RwLock");
121-
}
143+
assert!(!has_reached_max_readers(state), "too many active read locks on RwLock");
122144

123145
// Make sure the readers waiting bit is set before we go to sleep.
124146
if !has_readers_waiting(state) {
@@ -132,6 +154,7 @@ impl RwLock {
132154

133155
// Wait for the state to change.
134156
futex_wait(&self.state, state | READERS_WAITING, None);
157+
has_slept = true;
135158

136159
// Spin again after waking up.
137160
state = self.spin_read();
@@ -152,6 +175,9 @@ impl RwLock {
152175
}
153176
}
154177

178+
/// # Safety
179+
///
180+
/// The `RwLock` must be write-locked (single writer) in order to call this.
155181
#[inline]
156182
pub unsafe fn write_unlock(&self) {
157183
let state = self.state.fetch_sub(WRITE_LOCKED, Release) - WRITE_LOCKED;
@@ -163,6 +189,22 @@ impl RwLock {
163189
}
164190
}
165191

192+
/// # Safety
193+
///
194+
/// The `RwLock` must be write-locked (single writer) in order to call this.
195+
#[inline]
196+
pub unsafe fn downgrade(&self) {
197+
// Removes all write bits and adds a single read bit.
198+
let state = self.state.fetch_add(DOWNGRADE, Release);
199+
debug_assert!(is_write_locked(state), "RwLock must be write locked to call `downgrade`");
200+
201+
if has_readers_waiting(state) {
202+
// Since we had the exclusive lock, nobody else can unset this bit.
203+
self.state.fetch_sub(READERS_WAITING, Relaxed);
204+
futex_wake_all(&self.state);
205+
}
206+
}
207+
166208
#[cold]
167209
fn write_contended(&self) {
168210
let mut state = self.spin_write();

‎std/src/sys/sync/rwlock/no_threads.rs

+5
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,9 @@ impl RwLock {
6262
pub unsafe fn write_unlock(&self) {
6363
assert_eq!(self.mode.replace(0), -1);
6464
}
65+
66+
#[inline]
67+
pub unsafe fn downgrade(&self) {
68+
assert_eq!(self.mode.replace(1), -1);
69+
}
6570
}
There was a problem loading the remainder of the diff.

0 commit comments

Comments
 (0)
Failed to load comments.