diff --git a/.github/workflows/loom.yml b/.github/workflows/loom.yml index 016e9a0..23a7106 100644 --- a/.github/workflows/loom.yml +++ b/.github/workflows/loom.yml @@ -14,6 +14,7 @@ on: - 'asynchronix/src/ports/output/broadcaster/**' - 'asynchronix/src/ports/source/broadcaster.rs' - 'asynchronix/src/ports/source/broadcaster/**' + - 'asynchronix/src/util/cached_rw_lock.rs' - 'asynchronix/src/util/slot.rs' - 'asynchronix/src/util/sync_cell.rs' @@ -31,4 +32,4 @@ jobs: - name: Run cargo test (Loom) run: cargo test --tests --release env: - RUSTFLAGS: --cfg asynchronix_loom \ No newline at end of file + RUSTFLAGS: --cfg asynchronix_loom diff --git a/asynchronix/src/loom_exports.rs b/asynchronix/src/loom_exports.rs index d340569..df03e12 100644 --- a/asynchronix/src/loom_exports.rs +++ b/asynchronix/src/loom_exports.rs @@ -1,7 +1,8 @@ #[cfg(asynchronix_loom)] #[allow(unused_imports)] pub(crate) mod sync { - pub(crate) use loom::sync::{Arc, Mutex}; + pub(crate) use loom::sync::{Arc, LockResult, Mutex, MutexGuard}; + pub(crate) use std::sync::PoisonError; pub(crate) mod atomic { pub(crate) use loom::sync::atomic::{ @@ -12,7 +13,7 @@ pub(crate) mod sync { #[cfg(not(asynchronix_loom))] #[allow(unused_imports)] pub(crate) mod sync { - pub(crate) use std::sync::{Arc, Mutex}; + pub(crate) use std::sync::{Arc, LockResult, Mutex, MutexGuard, PoisonError}; pub(crate) mod atomic { pub(crate) use std::sync::atomic::{ diff --git a/asynchronix/src/ports.rs b/asynchronix/src/ports.rs index c764302..60c6821 100644 --- a/asynchronix/src/ports.rs +++ b/asynchronix/src/ports.rs @@ -19,33 +19,55 @@ //! //! #### Example //! -//! The outputs in this example are clones of each other and remain therefore -//! always connected to the same inputs. For an example usage of outputs cloning -//! in submodels assemblies, see the [`assembly example`][assembly]. +//! This example demonstrates a submodel inside a parent model. The output of +//! the submodel is a clone of the parent model output. Both outputs remain +//! therefore always connected to the same inputs. +//! +//! For a more comprehensive example demonstrating output cloning in submodels +//! assemblies, see the [`assembly example`][assembly]. //! //! [assembly]: //! https://github.com/asynchronics/asynchronix/tree/main/asynchronix/examples/assembly.rs //! //! ``` -//! use asynchronix::model::Model; +//! use asynchronix::model::{Model, SetupContext}; //! use asynchronix::ports::Output; +//! use asynchronix::simulation::Mailbox; //! -//! pub struct MyModel { -//! pub output_a: Output, -//! pub output_b: Output, +//! pub struct ChildModel { +//! pub output: Output, //! } //! -//! impl MyModel { +//! impl ChildModel { //! pub fn new() -> Self { -//! let output: Output<_> = Default::default(); //! Self { -//! output_a: output.clone(), -//! output_b: output, +//! output: Default::default(), //! } //! } //! } //! -//! impl Model for MyModel {} +//! impl Model for ChildModel {} +//! +//! pub struct ParentModel { +//! pub output: Output, +//! } +//! +//! impl ParentModel { +//! pub fn new() -> Self { +//! Self { +//! output: Default::default(), +//! } +//! } +//! } +//! +//! impl Model for ParentModel { +//! fn setup(&mut self, setup_context: &SetupContext) { +//! let mut child = ChildModel::new(); +//! let child_mbox = Mailbox::new(); +//! child.output = self.output.clone(); +//! setup_context.add_model(child, child_mbox); +//! } +//! } //! ``` mod input; diff --git a/asynchronix/src/util/cached_rw_lock.rs b/asynchronix/src/util/cached_rw_lock.rs index eba8bf1..d2a9125 100644 --- a/asynchronix/src/util/cached_rw_lock.rs +++ b/asynchronix/src/util/cached_rw_lock.rs @@ -1,6 +1,7 @@ use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, LockResult, Mutex, MutexGuard, PoisonError}; + +use crate::loom_exports::sync::atomic::{AtomicUsize, Ordering}; +use crate::loom_exports::sync::{Arc, LockResult, Mutex, MutexGuard, PoisonError}; /// A cached read-write lock. /// @@ -13,10 +14,9 @@ use std::sync::{Arc, LockResult, Mutex, MutexGuard, PoisonError}; /// synchronization. #[derive(Clone)] pub(crate) struct CachedRwLock { - local: T, - local_epoch: usize, - shared: Arc>, - epoch: Arc, + value: T, + epoch: usize, + shared: Arc>, } impl CachedRwLock { @@ -24,61 +24,63 @@ impl CachedRwLock { pub(crate) fn new(t: T) -> Self { let shared = t.clone(); Self { - local: t, - local_epoch: 0, - shared: Arc::new(Mutex::new(shared)), - epoch: Arc::new(AtomicUsize::new(0)), + value: t, + epoch: 0, + shared: Arc::new(Shared { + value: Mutex::new(shared), + epoch: AtomicUsize::new(0), + }), } } /// Gives access to the local cache without synchronization. pub(crate) fn read_unsync(&self) -> &T { - &self.local + &self.value } /// Synchronizes the local cache if it is behind the shared data and gives /// access to it. #[allow(dead_code)] pub(crate) fn read(&mut self) -> LockResult<&T> { - if self.epoch.load(Ordering::Relaxed) != self.local_epoch { - match self.shared.lock() { + if self.shared.epoch.load(Ordering::Relaxed) != self.epoch { + match self.shared.value.lock() { LockResult::Ok(shared) => { - self.local = shared.clone(); - self.local_epoch = self.epoch.load(Ordering::Relaxed) + self.value = shared.clone(); + self.epoch = self.shared.epoch.load(Ordering::Relaxed) } - LockResult::Err(_) => return LockResult::Err(PoisonError::new(&self.local)), + LockResult::Err(_) => return LockResult::Err(PoisonError::new(&self.value)), } } - LockResult::Ok(&self.local) + LockResult::Ok(&self.value) } /// Gives write access to the local cache without synchronization so it can /// be used as a scratchpad. #[allow(dead_code)] pub(crate) fn write_scratchpad_unsync(&mut self) -> &mut T { - &mut self.local + &mut self.value } /// Synchronizes the local cache if it is behind the shared data and gives /// write access to it so it can be used as a scratchpad. pub(crate) fn write_scratchpad(&mut self) -> LockResult<&mut T> { - if self.epoch.load(Ordering::Relaxed) != self.local_epoch { - match self.shared.lock() { + if self.shared.epoch.load(Ordering::Relaxed) != self.epoch { + match self.shared.value.lock() { LockResult::Ok(shared) => { - self.local = shared.clone(); - self.local_epoch = self.epoch.load(Ordering::Relaxed) + self.value = shared.clone(); + self.epoch = self.shared.epoch.load(Ordering::Relaxed) } - LockResult::Err(_) => return LockResult::Err(PoisonError::new(&mut self.local)), + LockResult::Err(_) => return LockResult::Err(PoisonError::new(&mut self.value)), } } - LockResult::Ok(&mut self.local) + LockResult::Ok(&mut self.value) } /// Acquires a write lock on the shared data. pub(crate) fn write(&mut self) -> LockResult> { - let guard = self.shared.lock(); - let epoch = self.epoch.load(Ordering::Relaxed) + 1; - self.epoch.store(epoch, Ordering::Relaxed); + let guard = self.shared.value.lock(); + let epoch = self.shared.epoch.load(Ordering::Relaxed) + 1; + self.shared.epoch.store(epoch, Ordering::Relaxed); match guard { LockResult::Ok(shared) => LockResult::Ok(CachedRwLockWriteGuard { guard: shared }), @@ -89,6 +91,11 @@ impl CachedRwLock { } } +struct Shared { + epoch: AtomicUsize, + value: Mutex, +} + /// Write guard. /// /// The lock is released when the guard is dropped. @@ -109,3 +116,59 @@ impl DerefMut for CachedRwLockWriteGuard<'_, T> { &mut self.guard } } + +#[cfg(all(test, asynchronix_loom))] +mod tests { + use super::*; + + use loom::model::Builder; + use loom::thread; + + #[test] + fn loom_cached_rw_lock_write() { + const DEFAULT_PREEMPTION_BOUND: usize = 4; + const ITERATIONS_NUMBER: usize = 5; + + let mut builder = Builder::new(); + if builder.preemption_bound.is_none() { + builder.preemption_bound = Some(DEFAULT_PREEMPTION_BOUND); + } + + builder.check(move || { + let mut writer0: CachedRwLock = CachedRwLock::new(0); + let mut writer1 = writer0.clone(); + let mut reader = writer0.clone(); + + let th_w = thread::spawn(move || { + for _ in 0..ITERATIONS_NUMBER { + let mut guard = writer0.write().unwrap(); + *guard = *guard + 1; + } + }); + + let th_r = thread::spawn(move || { + let mut value = 0; + let mut prev_value; + for _ in 0..ITERATIONS_NUMBER { + prev_value = value; + value = *reader.write_scratchpad().unwrap(); + assert!( + prev_value <= value, + "Previous value = {}, value = {}", + prev_value, + value + ); + assert_eq!(value, reader.epoch); + } + }); + + for _ in 0..ITERATIONS_NUMBER { + let mut guard = writer1.write().unwrap(); + *guard = *guard + 1; + } + + th_w.join().unwrap(); + th_r.join().unwrap(); + }); + } +}