Repository: tokio-rs/loom (Rust)

Issue:
Add support for RwLock
I notice that `loom::sync` has a [mock implementation of `std::sync::Mutex`](https://github.com/carllerche/loom/blob/master/src/sync/mutex.rs) but lacks a mock `std::sync::RwLock`. It would be nice to be able to test code using read-write locks with `loom`.


Discussion:
I'm interested in contributing to loom. This seems pretty straight forward if I model the `RwLock` implementation after the `Mutex` version. Mind if I start working on this?
@pop Sure, go ahead!



Patch:
diff --git a/src/lib.rs b/src/lib.rs
index fd956ae7..83dc0049 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -85,8 +85,8 @@
 //! case and control the scheduling.
 //!
 //! Tests must use the loom synchronization types, such as `Atomic*`, `Mutex`,
-//! `Condvar`, `thread::spawn`, etc. When writing a concurrent program, the
-//! `std` types should be used when running the program and the `loom` types
+//! `RwLock`, `Condvar`, `thread::spawn`, etc. When writing a concurrent program,
+//! the `std` types should be used when running the program and the `loom` types
 //! when running the test.
 //!
 //! One way to do this is via cfg flags.
diff --git a/src/rt/mod.rs b/src/rt/mod.rs
index b4ee7a53..52fd55bf 100644
--- a/src/rt/mod.rs
+++ b/src/rt/mod.rs
@@ -38,6 +38,9 @@ pub(crate) use self::mutex::Mutex;
 mod path;
 pub(crate) use self::path::Path;
 
+mod rwlock;
+pub(crate) use self::rwlock::RwLock;
+
 mod scheduler;
 pub(crate) use self::scheduler::Scheduler;
 
diff --git a/src/rt/object.rs b/src/rt/object.rs
index 59a28380..7c20e3b7 100644
--- a/src/rt/object.rs
+++ b/src/rt/object.rs
@@ -120,6 +120,9 @@ objects! {
     // State associated with a modeled thread notifier.
     Notify(rt::notify::State),
 
+    // State associated with an RwLock
+    RwLock(rt::rwlock::State),
+
     // Tracks access to a memory cell
     Cell(rt::cell::State),
 }
@@ -197,6 +200,7 @@ impl Store {
             Entry::Mutex(entry) => entry.last_dependent_access(),
             Entry::Condvar(entry) => entry.last_dependent_access(),
             Entry::Notify(entry) => entry.last_dependent_access(),
+            Entry::RwLock(entry) => entry.last_dependent_access(),
             obj => panic!(
                 "object is not branchable {:?}; ref = {:?}",
                 obj, operation.obj
@@ -218,6 +222,7 @@ impl Store {
             Entry::Mutex(entry) => entry.set_last_access(path_id, dpor_vv),
             Entry::Condvar(entry) => entry.set_last_access(path_id, dpor_vv),
             Entry::Notify(entry) => entry.set_last_access(path_id, dpor_vv),
+            Entry::RwLock(entry) => entry.set_last_access(path_id, dpor_vv),
             _ => panic!("object is not branchable"),
         }
     }
diff --git a/src/rt/rwlock.rs b/src/rt/rwlock.rs
new file mode 100644
index 00000000..51a5ec2d
--- /dev/null
+++ b/src/rt/rwlock.rs
@@ -0,0 +1,242 @@
+use crate::rt::object;
+use crate::rt::{thread, Access, Execution, Synchronize, VersionVec};
+
+use std::collections::HashSet;
+use std::sync::atomic::Ordering::{Acquire, Release};
+
+#[derive(Debug, Copy, Clone)]
+pub(crate) struct RwLock {
+    state: object::Ref<State>,
+}
+
+#[derive(Debug, PartialEq)]
+enum Locked {
+    Read(HashSet<thread::Id>),
+    Write(thread::Id),
+}
+
+#[derive(Debug)]
+pub(super) struct State {
+    /// A single `thread::Id` when Write locked.
+    /// A set of `thread::Id` when Read locked.
+    lock: Option<Locked>,
+
+    /// Tracks write access to the rwlock.
+    last_access: Option<Access>,
+
+    /// Causality transfers between threads
+    synchronize: Synchronize,
+}
+
+impl RwLock {
+    /// Common RwLock function
+    pub(crate) fn new() -> RwLock {
+        super::execution(|execution| {
+            let state = execution.objects.insert(State {
+                lock: None,
+                last_access: None,
+                synchronize: Synchronize::new(),
+            });
+
+            RwLock { state }
+        })
+    }
+
+    /// Acquire the read lock.
+    /// Fail to acquire read lock if already *write* locked.
+    pub(crate) fn acquire_read_lock(&self) {
+        self.state.branch_acquire(self.is_write_locked());
+        assert!(
+            self.post_acquire_read_lock(),
+            "expected to be able to acquire read lock"
+        );
+    }
+
+    /// Acquire write lock.
+    /// Fail to acquire write lock if either read or write locked.
+    pub(crate) fn acquire_write_lock(&self) {
+        self.state
+            .branch_acquire(self.is_write_locked() || self.is_read_locked());
+        assert!(
+            self.post_acquire_write_lock(),
+            "expected to be able to acquire write lock"
+        );
+    }
+
+    pub(crate) fn try_acquire_read_lock(&self) -> bool {
+        self.state.branch_opaque();
+        self.post_acquire_read_lock()
+    }
+
+    pub(crate) fn try_acquire_write_lock(&self) -> bool {
+        self.state.branch_opaque();
+        self.post_acquire_write_lock()
+    }
+
+    pub(crate) fn release_read_lock(&self) {
+        super::execution(|execution| {
+            let state = self.state.get_mut(&mut execution.objects);
+
+            state.lock = None;
+
+            state
+                .synchronize
+                .sync_store(&mut execution.threads, Release);
+
+            // Establish sequential consistency between the lock's operations.
+            execution.threads.seq_cst();
+
+            let thread_id = execution.threads.active_id();
+
+            self.unlock_threads(execution, thread_id);
+        });
+    }
+
+    pub(crate) fn release_write_lock(&self) {
+        super::execution(|execution| {
+            let state = self.state.get_mut(&mut execution.objects);
+
+            state.lock = None;
+
+            state
+                .synchronize
+                .sync_store(&mut execution.threads, Release);
+
+            // Establish sequential consistency between the lock's operations.
+            execution.threads.seq_cst();
+
+            let thread_id = execution.threads.active_id();
+
+            self.unlock_threads(execution, thread_id);
+        });
+    }
+
+    fn lock_out_threads(&self, execution: &mut Execution, thread_id: thread::Id) {
+        // TODO: This and the following function look very similar.
+        // Refactor the two to DRY the code.
+        for (id, thread) in execution.threads.iter_mut() {
+            if id == thread_id {
+                continue;
+            }
+
+            let obj = thread
+                .operation
+                .as_ref()
+                .map(|operation| operation.object());
+
+            if obj == Some(self.state.erase()) {
+                thread.set_blocked();
+            }
+        }
+    }
+
+    fn unlock_threads(&self, execution: &mut Execution, thread_id: thread::Id) {
+        // TODO: This and the above function look very similar.
+        // Refactor the two to DRY the code.
+        for (id, thread) in execution.threads.iter_mut() {
+            if id == thread_id {
+                continue;
+            }
+
+            let obj = thread
+                .operation
+                .as_ref()
+                .map(|operation| operation.object());
+
+            if obj == Some(self.state.erase()) {
+                thread.set_runnable();
+            }
+        }
+    }
+
+    /// Returns `true` if RwLock is read locked
+    fn is_read_locked(&self) -> bool {
+        super::execution(
+            |execution| match self.state.get(&mut execution.objects).lock {
+                Some(Locked::Read(_)) => true,
+                _ => false,
+            },
+        )
+    }
+
+    /// Returns `true` if RwLock is write locked.
+    fn is_write_locked(&self) -> bool {
+        super::execution(
+            |execution| match self.state.get(&mut execution.objects).lock {
+                Some(Locked::Write(_)) => true,
+                _ => false,
+            },
+        )
+    }
+
+    fn post_acquire_read_lock(&self) -> bool {
+        super::execution(|execution| {
+            let mut state = self.state.get_mut(&mut execution.objects);
+            let thread_id = execution.threads.active_id();
+
+            // Set the lock to the current thread
+            let mut already_locked = false;
+            state.lock = match state.lock.take() {
+                None => {
+                    let mut threads: HashSet<thread::Id> = HashSet::new();
+                    threads.insert(thread_id);
+                    Some(Locked::Read(threads))
+                }
+                Some(Locked::Read(mut threads)) => {
+                    threads.insert(thread_id);
+                    Some(Locked::Read(threads))
+                }
+                Some(Locked::Write(writer)) => {
+                    already_locked = true;
+                    Some(Locked::Write(writer))
+                }
+            };
+
+            // The RwLock is already Write locked, so we cannot acquire a read lock on it.
+            if already_locked {
+                return false;
+            }
+
+            dbg!(state.synchronize.sync_load(&mut execution.threads, Acquire));
+
+            execution.threads.seq_cst();
+
+            // Block all writer threads from attempting to acquire the RwLock
+            self.lock_out_threads(execution, thread_id);
+
+            true
+        })
+    }
+
+    fn post_acquire_write_lock(&self) -> bool {
+        super::execution(|execution| {
+            let state = self.state.get_mut(&mut execution.objects);
+            let thread_id = execution.threads.active_id();
+
+            // Set the lock to the current thread
+            state.lock = match state.lock {
+                Some(Locked::Read(_)) => return false,
+                _ => Some(Locked::Write(thread_id)),
+            };
+
+            dbg!(state.synchronize.sync_load(&mut execution.threads, Acquire));
+
+            // Establish sequential consistency between locks
+            execution.threads.seq_cst();
+
+            // Block all other threads attempting to acquire rwlock
+
+            true
+        })
+    }
+}
+
+impl State {
+    pub(crate) fn last_dependent_access(&self) -> Option<&Access> {
+        self.last_access.as_ref()
+    }
+
+    pub(crate) fn set_last_access(&mut self, path_id: usize, version: &VersionVec) {
+        Access::set_or_create(&mut self.last_access, path_id, version)
+    }
+}
diff --git a/src/sync/mod.rs b/src/sync/mod.rs
index 58ea469b..b3cb759a 100644
--- a/src/sync/mod.rs
+++ b/src/sync/mod.rs
@@ -5,10 +5,12 @@ pub mod atomic;
 mod condvar;
 mod mutex;
 mod notify;
+mod rwlock;
 
 pub use self::arc::Arc;
 pub use self::condvar::{Condvar, WaitTimeoutResult};
 pub use self::mutex::{Mutex, MutexGuard};
 pub use self::notify::Notify;
+pub use self::rwlock::{RwLock, RwLockReadGuard, RwLockWriteGuard};
 
 pub use std::sync::{LockResult, TryLockResult};
diff --git a/src/sync/rwlock.rs b/src/sync/rwlock.rs
new file mode 100644
index 00000000..c2eeb6db
--- /dev/null
+++ b/src/sync/rwlock.rs
@@ -0,0 +1,142 @@
+use crate::rt;
+
+use std::{
+    ops,
+    sync::{LockResult, TryLockError, TryLockResult},
+};
+
+/// Mock implementatoin of `std::sync::RwLock`
+#[derive(Debug)]
+pub struct RwLock<T> {
+    object: rt::RwLock,
+    data: std::sync::RwLock<T>,
+}
+
+/// Mock implementation of `std::sync::RwLockReadGuard`
+#[derive(Debug)]
+pub struct RwLockReadGuard<'a, T> {
+    lock: &'a RwLock<T>,
+    data: Option<std::sync::RwLockReadGuard<'a, T>>,
+}
+
+/// Mock implementation of `std::sync::rwLockWriteGuard`
+#[derive(Debug)]
+pub struct RwLockWriteGuard<'a, T> {
+    lock: &'a RwLock<T>,
+    /// `data` is an Option so that the Drop impl can drop the std guard and release the std lock
+    /// before releasing the loom mock lock, as that might cause another thread to acquire the lock
+    data: Option<std::sync::RwLockWriteGuard<'a, T>>,
+}
+
+impl<T> RwLock<T> {
+    /// Creates a new rwlock in an unlocked state ready for use.
+    pub fn new(data: T) -> RwLock<T> {
+        RwLock {
+            data: std::sync::RwLock::new(data),
+            object: rt::RwLock::new(),
+        }
+    }
+
+    /// Locks this rwlock with shared read access, blocking the current
+    /// thread until it can be acquired.
+    ///
+    /// The calling thread will be blocked until there are no more writers
+    /// which hold the lock. There may be other readers currently inside the
+    /// lock when this method returns. This method does not provide any
+    /// guarantees with respect to the ordering of whether contentious readers
+    /// or writers will acquire the lock first.
+    pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
+        self.object.acquire_read_lock();
+
+        Ok(RwLockReadGuard {
+            lock: self,
+            data: Some(self.data.read().unwrap()),
+        })
+    }
+
+    /// Attempts to acquire this rwlock with shared read access.
+    ///
+    /// If the access could not be granted at this time, then Err is returned.
+    /// Otherwise, an RAII guard is returned which will release the shared
+    /// access when it is dropped.
+    ///
+    /// This function does not block.
+    pub fn try_read(&self) -> TryLockResult<RwLockReadGuard<'_, T>> {
+        if self.object.try_acquire_read_lock() {
+            Ok(RwLockReadGuard {
+                lock: self,
+                data: Some(self.data.read().unwrap()),
+            })
+        } else {
+            Err(TryLockError::WouldBlock)
+        }
+    }
+
+    /// Locks this rwlock with exclusive write access, blocking the current
+    /// thread until it can be acquired.
+    ///
+    /// This function will not return while other writers or other readers
+    /// currently have access to the lock.
+    pub fn write(&self) -> LockResult<RwLockWriteGuard<'_, T>> {
+        self.object.acquire_write_lock();
+
+        Ok(RwLockWriteGuard {
+            lock: self,
+            data: Some(self.data.write().unwrap()),
+        })
+    }
+
+    /// Attempts to lock this rwlock with exclusive write access.
+    ///
+    /// If the lock could not be acquired at this time, then Err is returned.
+    /// Otherwise, an RAII guard is returned which will release the lock when
+    /// it is dropped.
+    ///
+    /// This function does not block.
+    pub fn try_write(&self) -> TryLockResult<RwLockWriteGuard<'_, T>> {
+        if self.object.try_acquire_write_lock() {
+            Ok(RwLockWriteGuard {
+                lock: self,
+                data: Some(self.data.write().unwrap()),
+            })
+        } else {
+            Err(TryLockError::WouldBlock)
+        }
+    }
+}
+
+impl<'a, T> ops::Deref for RwLockReadGuard<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        self.data.as_ref().unwrap().deref()
+    }
+}
+
+impl<'a, T: 'a> Drop for RwLockReadGuard<'a, T> {
+    fn drop(&mut self) {
+        self.data = None;
+        self.lock.object.release_read_lock()
+    }
+}
+
+impl<'a, T> ops::Deref for RwLockWriteGuard<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &T {
+        self.data.as_ref().unwrap().deref()
+    }
+}
+
+impl<'a, T> ops::DerefMut for RwLockWriteGuard<'a, T> {
+    fn deref_mut(&mut self) -> &mut T {
+        self.data.as_mut().unwrap().deref_mut()
+    }
+}
+
+impl<'a, T: 'a> Drop for RwLockWriteGuard<'a, T> {
+    fn drop(&mut self) {
+        self.data = None;
+        self.lock.object.release_write_lock()
+    }
+}


Test patch:
diff --git a/tests/rwlock.rs b/tests/rwlock.rs
new file mode 100644
index 00000000..047be8d8
--- /dev/null
+++ b/tests/rwlock.rs
@@ -0,0 +1,60 @@
+use loom;
+
+use loom::{
+    sync::{Arc, RwLock},
+    thread,
+};
+
+#[test]
+fn rwlock_read() {
+    loom::model(|| {
+        let lock = Arc::new(RwLock::new(1));
+        let c_lock = lock.clone();
+
+        let n = lock.read().unwrap();
+        assert_eq!(*n, 1);
+
+        thread::spawn(move || {
+            let r = c_lock.read();
+            assert!(r.is_ok());
+        })
+        .join()
+        .unwrap();
+    });
+}
+
+#[test]
+fn rwlock_try_read() {
+    loom::model(|| {
+        let lock = RwLock::new(1);
+
+        match lock.try_read() {
+            Ok(n) => assert_eq!(*n, 1),
+            Err(_) => unreachable!(),
+        };
+    });
+}
+
+#[test]
+fn rwlock_write() {
+    loom::model(|| {
+        let lock = RwLock::new(1);
+
+        let mut n = lock.write().unwrap();
+        *n = 2;
+
+        assert!(lock.try_read().is_err());
+    });
+}
+
+#[test]
+fn rwlock_try_write() {
+    loom::model(|| {
+        let lock = RwLock::new(1);
+
+        let n = lock.read().unwrap();
+        assert_eq!(*n, 1);
+
+        assert!(lock.try_write().is_err());
+    });
+}