Skip to content

Commit

Permalink
test: add new testing trait for locks
Browse files Browse the repository at this point in the history
  • Loading branch information
pedromfedricci committed Dec 7, 2024
1 parent 1ccf1a8 commit 09b20b7
Show file tree
Hide file tree
Showing 6 changed files with 360 additions and 116 deletions.
9 changes: 9 additions & 0 deletions src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,15 @@ pub mod hint {
pub use loom::hint::spin_loop;
}

#[cfg(test)]
pub mod sync {
#[cfg(not(all(loom, test)))]
pub use std::sync::Arc;

#[cfg(all(loom, test))]
pub use loom::sync::Arc;
}

pub mod thread {
#[cfg(all(any(feature = "yield", test), not(loom)))]
pub use std::thread::yield_now;
Expand Down
14 changes: 6 additions & 8 deletions src/inner/raw/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ impl<T: ?Sized, L: Lock, W: Wait, F: Fairness> Mutex<T, L, W, F> {
///
/// The returned guard instance **must** be dropped, that is, it **must not**
/// be "forgotten" (e.g. `core::mem::forget`), or being targeted of any
/// other operation hat would prevent it from executing its `drop` call.
/// other operation that would prevent it from executing its `drop` call.
unsafe fn lock_with<'a>(&'a self, n: &'a mut MutexNode<L>) -> MutexGuard<'a, T, L, W, F> {
let node = n.initialize();
let pred = self.tail.swap(node.as_ptr(), AcqRel);
Expand Down Expand Up @@ -665,24 +665,22 @@ impl<T: ?Sized, L: Lock, W: Wait, F: Fairness> AsDerefMutWithMut for OptionGuard
}

#[cfg(not(tarpaulin_include))]
impl<'a, T: ?Sized + Debug, L: Lock, W: Wait, F: Fairness> Debug for MutexGuard<'a, T, L, W, F> {
impl<T: ?Sized + Debug, L: Lock, W: Wait, F: Fairness> Debug for MutexGuard<'_, T, L, W, F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.with(|data| data.fmt(f))
}
}

#[cfg(not(tarpaulin_include))]
impl<'a, T: ?Sized + Display, L: Lock, W: Wait, F: Fairness> Display
for MutexGuard<'a, T, L, W, F>
{
impl<T: ?Sized + Display, L: Lock, W: Wait, F: Fairness> Display for MutexGuard<'_, T, L, W, F> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.with(|data| data.fmt(f))
}
}

#[cfg(not(all(loom, test)))]
#[cfg(not(tarpaulin_include))]
impl<'a, T: ?Sized, L: Lock, W: Wait, F: Fairness> Deref for MutexGuard<'a, T, L, W, F> {
impl<T: ?Sized, L: Lock, W: Wait, F: Fairness> Deref for MutexGuard<'_, T, L, W, F> {
type Target = T;

/// Dereferences the guard to access the underlying data.
Expand All @@ -694,15 +692,15 @@ impl<'a, T: ?Sized, L: Lock, W: Wait, F: Fairness> Deref for MutexGuard<'a, T, L

#[cfg(not(all(loom, test)))]
#[cfg(not(tarpaulin_include))]
impl<'a, T: ?Sized, L: Lock, W: Wait, F: Fairness> DerefMut for MutexGuard<'a, T, L, W, F> {
impl<T: ?Sized, L: Lock, W: Wait, F: Fairness> DerefMut for MutexGuard<'_, T, L, W, F> {
/// Mutably dereferences the guard to access the underlying data.
fn deref_mut(&mut self) -> &mut T {
// SAFETY: A guard instance holds the lock locked.
unsafe { &mut *self.lock.data.get() }
}
}

impl<'a, T: ?Sized, L: Lock, W: Wait, F: Fairness> Drop for MutexGuard<'a, T, L, W, F> {
impl<T: ?Sized, L: Lock, W: Wait, F: Fairness> Drop for MutexGuard<'_, T, L, W, F> {
fn drop(&mut self) {
// SAFETY: At most one guard drop call may be running at any given time
// for its associated mutex. In that period, the unlocking thread is
Expand Down
148 changes: 120 additions & 28 deletions src/loom.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,125 @@
#[cfg(feature = "barging")]
pub use guard::{Guard, GuardDeref, GuardDerefMut};

#[cfg(feature = "barging")]
mod guard {
use core::marker::PhantomData;
use core::ops::{Deref, DerefMut};

use loom::cell::{ConstPtr, MutPtr, UnsafeCell};

/// A trait for guard types that protect the access to the underlying data
/// behind Loom's [`UnsafeCell`].
///
/// # Safety
///
/// Must guarantee that an instance of the guard is the only access point
/// to the underlying data through all its lifetime.
pub unsafe trait Guard: Sized {
/// The target type after dereferencing [`GuardDeref`] or [`GuardDerefMut`].
type Target: ?Sized;

/// Returns a shared reference to the underlying [`UnsafeCell`].
fn get(&self) -> &UnsafeCell<Self::Target>;

/// Get a Loom immutable pointer bounded by this guard lifetime.
fn get_ref(&self) -> GuardDeref<'_, Self> {
GuardDeref::new(self)
}

/// Get a Loom mutable pointer bounded by this guard lifetime.
fn get_mut(&mut self) -> GuardDerefMut<'_, Self> {
GuardDerefMut::new(self)
}
}

/// A Loom immutable pointer borrowed from a guard instance.
pub struct GuardDeref<'a, G: Guard> {
ptr: ConstPtr<G::Target>,
marker: PhantomData<(&'a G::Target, &'a G)>,
}

impl<G: Guard> GuardDeref<'_, G> {
fn new(guard: &G) -> Self {
let ptr = guard.get().get();
Self { ptr, marker: PhantomData }
}
}

impl<G: Guard> Deref for GuardDeref<'_, G> {
type Target = G::Target;

fn deref(&self) -> &Self::Target {
// SAFETY: Our lifetime is bounded by the guard borrow.
unsafe { self.ptr.deref() }
}
}

/// A Loom mutable pointer borrowed from a guard instance.
pub struct GuardDerefMut<'a, G: Guard> {
ptr: MutPtr<G::Target>,
marker: PhantomData<(&'a mut G::Target, &'a mut G)>,
}

impl<G: Guard> GuardDerefMut<'_, G> {
#[allow(clippy::needless_pass_by_ref_mut)]
fn new(guard: &mut G) -> Self {
let ptr = guard.get().get_mut();
Self { ptr, marker: PhantomData }
}
}

impl<G: Guard> Deref for GuardDerefMut<'_, G> {
type Target = G::Target;

fn deref(&self) -> &Self::Target {
// SAFETY: Our lifetime is bounded by the guard borrow.
unsafe { self.ptr.deref() }
}
}

impl<G: Guard> DerefMut for GuardDerefMut<'_, G> {
fn deref_mut(&mut self) -> &mut Self::Target {
// SAFETY: Our lifetime is bounded by the guard borrow.
unsafe { self.ptr.deref() }
}
}
}

pub mod models {
use core::array;

use loom::sync::Arc;
use loom::{model, thread};

use crate::test::{AsDeref, AsDerefMut, LockThen, TryLockThen};
use crate::test::{get, inc, try_inc, Int};
use crate::test::{LockThen, TryLockThen};

/// Get a copy of the shared integer, converting it to usize.
///
/// Panics if the cast fails.
fn get_unwrap<L>(lock: &Arc<L>) -> usize
where
L: LockThen<Target = Int>,
{
get(lock).try_into().unwrap()
}

type Int = usize;
// TODO: Three or more threads make lock models run for too long. It would
// be nice to run a lock model with at least three threads because that
// would cover a queue with head, tail and at least one more queue node
// instead of a queue with just head and tail.
const LOCKS: Int = 2;
const TRY_LOCKS: Int = 3;

/// Increments a shared integer.
fn inc<L: LockThen<Target = Int>>(lock: &Arc<L>) {
lock.lock_then(|mut data| *data.as_deref_mut() += 1);
}

/// Tries to increment a shared integer.
fn try_inc<L: TryLockThen<Target = Int>>(lock: &Arc<L>) {
lock.try_lock_then(|opt| opt.map(|mut data| *data.as_deref_mut() += 1));
}

/// Get the shared integer.
fn get<L: LockThen<Target = Int>>(lock: &Arc<L>) -> Int {
lock.lock_then(|data| *data.as_deref())
}
const LOCKS: usize = 2;
const TRY_LOCKS: usize = 3;

/// Evaluates that concurrent `try_lock` calls will serialize all mutations
/// against the shared data, therefore no data races.
pub fn try_lock_join<L: TryLockThen<Target = Int> + 'static>() {
pub fn try_lock_join<L>()
where
L: TryLockThen<Target = Int> + 'static,
{
model(|| {
const RUNS: Int = TRY_LOCKS;
const RUNS: usize = TRY_LOCKS;
let lock = Arc::new(L::new(0));
let handles: [_; RUNS] = array::from_fn(|_| {
let lock = Arc::clone(&lock);
Expand All @@ -42,16 +128,19 @@ pub mod models {
for handle in handles {
handle.join().unwrap();
}
let value = get(&lock);
let value = get_unwrap(&lock);
assert!((1..=RUNS).contains(&value));
});
}

/// Evaluates that concurrent `lock` calls will serialize all mutations
/// against the shared data, therefore no data races.
pub fn lock_join<L: LockThen<Target = Int> + 'static>() {
pub fn lock_join<L>()
where
L: LockThen<Target = Int> + 'static,
{
model(|| {
const RUNS: Int = LOCKS;
const RUNS: usize = LOCKS;
let lock = Arc::new(L::new(0));
let handles: [_; RUNS] = array::from_fn(|_| {
let lock = Arc::clone(&lock);
Expand All @@ -60,16 +149,19 @@ pub mod models {
for handle in handles {
handle.join().unwrap();
}
let value = get(&lock);
let value = get_unwrap(&lock);
assert_eq!(RUNS, value);
});
}

/// Evaluates that concurrent `lock` and `try_lock` calls will serialize
/// all mutations against the shared data, therefore no data races.
pub fn mixed_lock_join<L: TryLockThen<Target = Int> + 'static>() {
pub fn mixed_lock_join<L>()
where
L: TryLockThen<Target = Int> + 'static,
{
model(|| {
const RUNS: Int = LOCKS;
const RUNS: usize = LOCKS;
let lock = Arc::new(L::new(0));
let handles: [_; RUNS] = array::from_fn(|run| {
let lock = Arc::clone(&lock);
Expand All @@ -79,7 +171,7 @@ pub mod models {
for handle in handles {
handle.join().unwrap();
}
let value = get(&lock);
let value = get_unwrap(&lock);
assert!((1..=RUNS).contains(&value));
});
}
Expand Down
37 changes: 30 additions & 7 deletions src/raw/mutex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::relax::{Relax, RelaxWait};
use crate::xoshiro::LocalGenerator;

#[cfg(test)]
use crate::test::{LockNew, LockThen, TryLockThen};
use crate::test::{LockNew, LockThen, LockWithThen, TryLockThen, TryLockWithThen};

/// A locally-accessible record for forming the waiting queue.
///
Expand Down Expand Up @@ -502,12 +502,39 @@ impl<T: ?Sized, R> LockNew for Mutex<T, R> {
}

#[cfg(test)]
impl<T: ?Sized, R: Relax> LockThen for Mutex<T, R> {
type Guard<'a> = &'a mut Self::Target
impl<T: ?Sized, R: Relax> LockWithThen for Mutex<T, R> {
type Node = MutexNode;

type Guard<'a>
= &'a mut Self::Target
where
Self: 'a,
Self::Target: 'a;

fn lock_with_then<F, Ret>(&self, node: &mut Self::Node, f: F) -> Ret
where
F: FnOnce(&mut Self::Target) -> Ret,
{
self.lock_with_then(node, f)
}
}

#[cfg(test)]
impl<T: ?Sized, R: Relax> TryLockWithThen for Mutex<T, R> {
fn try_lock_with_then<F, Ret>(&self, node: &mut Self::Node, f: F) -> Ret
where
F: FnOnce(Option<&mut Self::Target>) -> Ret,
{
self.try_lock_with_then(node, f)
}

fn is_locked(&self) -> bool {
self.is_locked()
}
}

#[cfg(test)]
impl<T: ?Sized, R: Relax> LockThen for Mutex<T, R> {
fn lock_then<F, Ret>(&self, f: F) -> Ret
where
F: FnOnce(&mut Self::Target) -> Ret,
Expand All @@ -524,10 +551,6 @@ impl<T: ?Sized, R: Relax> TryLockThen for Mutex<T, R> {
{
self.try_lock_then(f)
}

fn is_locked(&self) -> bool {
self.is_locked()
}
}

#[cfg(all(not(loom), test))]
Expand Down
Loading

0 comments on commit 09b20b7

Please sign in to comment.