Skip to content

Commit 907250d

Browse files
committed
add atomic correctness test for rwlock downgrade
1 parent be4cbc7 commit 907250d

File tree

1 file changed

+83
-0
lines changed

1 file changed

+83
-0
lines changed

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::hint::spin_loop;
12
use crate::sync::atomic::{AtomicUsize, Ordering};
23
use crate::sync::mpsc::channel;
34
use crate::sync::{
@@ -537,6 +538,9 @@ fn test_downgrade_readers() {
537538
const R: usize = 16;
538539
const N: usize = 1000;
539540

541+
// Starts up 1 writing thread and `R` reader threads.
542+
// The writer thread will constantly update the value inside the `RwLock`, and this test will
543+
// only pass if every reader observes all values between 0 and `N`.
540544
let r = Arc::new(RwLock::new(0));
541545
let b = Arc::new(Barrier::new(R + 1));
542546

@@ -582,3 +586,82 @@ fn test_downgrade_readers() {
582586
});
583587
}
584588
}
589+
590+
#[test]
591+
fn test_downgrade_atomic() {
592+
const R: usize = 16;
593+
594+
let r = Arc::new(RwLock::new(0));
595+
// The number of reader threads that observe the correct value.
596+
let observers = Arc::new(AtomicUsize::new(0));
597+
598+
let w = r.clone();
599+
let mut main_write_guard = w.write().unwrap();
600+
601+
// While the current thread is holding the write lock, spawn several reader threads and an evil
602+
// writer thread.
603+
// Each of the threads will attempt to read the `RwLock` and go to sleep because we have the
604+
// write lock.
605+
// We need at least 1 reader thread to observe what the main thread writes, otherwise that means
606+
// the evil writer thread got in front of every single reader.
607+
608+
// FIXME
609+
// Should we actually require that every reader observe the first change?
610+
// This is a matter of protocol rather than correctness...
611+
612+
let mut reader_handles = Vec::with_capacity(R);
613+
614+
for _ in 0..R {
615+
let r = r.clone();
616+
let observers = observers.clone();
617+
let handle = thread::spawn(move || {
618+
// Will go to sleep since the main thread initially has the write lock.
619+
let read_guard = r.read().unwrap();
620+
if *read_guard == 1 {
621+
observers.fetch_add(1, Ordering::Relaxed);
622+
}
623+
});
624+
625+
reader_handles.push(handle);
626+
}
627+
628+
let evil = r.clone();
629+
let evil_handle = thread::spawn(move || {
630+
// Will go to sleep since the main thread initially has the write lock.
631+
let mut evil_guard = evil.write().unwrap();
632+
*evil_guard = 2;
633+
});
634+
635+
// FIXME Come up with a better way to make sure everyone is sleeping.
636+
// Make sure that everyone else is actually sleeping.
637+
let spin = 1000000;
638+
for _ in 0..spin {
639+
spin_loop();
640+
}
641+
642+
// Once everyone is asleep, set the value to 1.
643+
*main_write_guard = 1;
644+
645+
// Atomically downgrade the write guard into a read guard.
646+
// This should wake up all of the reader threads, and allow them to also take the read lock.
647+
let main_read_guard = RwLockWriteGuard::downgrade(main_write_guard);
648+
649+
// If the above is not atomic, then it is possible for the evil thread to get in front of the
650+
// readers and change the value to 2 instead.
651+
assert_eq!(*main_read_guard, 1, "`downgrade` was not atomic");
652+
653+
// By dropping all of the read guards, we allow the evil thread to make the change.
654+
drop(main_read_guard);
655+
656+
for handle in reader_handles {
657+
handle.join().unwrap();
658+
}
659+
660+
// Wait for the evil thread to set the value to 2.
661+
evil_handle.join().unwrap();
662+
663+
let final_check = r.read().unwrap();
664+
assert_eq!(*final_check, 2);
665+
666+
assert!(observers.load(Ordering::Relaxed) > 0, "No readers observed the correct value");
667+
}

0 commit comments

Comments
 (0)