From f94dd5bf47c9a3d1fe63a606c18d11e0cd85ab8e Mon Sep 17 00:00:00 2001 From: John Bell Date: Wed, 8 Nov 2023 00:16:45 +0000 Subject: [PATCH] Add no-std support (#3) * Add no-std support * Update CI and version number * Add workflows for loom and miri --- .github/workflows/main.yml | 40 ++++++++++- Cargo.toml | 10 ++- src/domain/list.rs | 8 ++- src/domain/mod.rs | 26 +++++--- src/domain/reclaim_strategy.rs | 118 ++++++++++++++++++++++++++++++--- src/hazard_ptr.rs | 4 +- src/lib.rs | 25 ++++--- src/sync.rs | 6 +- 8 files changed, 196 insertions(+), 41 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 947eba8..e63660a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,7 @@ jobs: strategy: matrix: rust: [ stable, nightly ] + features: [ --no-default-features, --all-features ] steps: - run: sudo apt install libwayland-cursor0 libxkbcommon-dev libwayland-dev - uses: actions/checkout@v2 @@ -24,6 +25,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: check + args: ${{ matrix.features }} test: name: Test Suite @@ -31,6 +33,7 @@ jobs: strategy: matrix: rust: [ stable, nightly ] + features: [ --no-default-features, --all-features ] steps: - run: sudo apt install libwayland-cursor0 libxkbcommon-dev libwayland-dev - uses: actions/checkout@v2 @@ -42,6 +45,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test + args: ${{ matrix.features }} fmt: name: Rustfmt @@ -69,6 +73,7 @@ jobs: strategy: matrix: rust: [ stable, nightly ] + features: [ --no-default-features, --all-features ] steps: - run: sudo apt install libwayland-cursor0 libxkbcommon-dev libwayland-dev - uses: actions/checkout@v2 @@ -81,4 +86,37 @@ jobs: - uses: actions-rs/cargo@v1 with: command: clippy - # args: -- -D warnings + args: ${{ matrix.features }} -- -D warnings + + loom: + name: Loom + runs-on: ubuntu-latest + strategy: + matrix: + rust: [ stable, nightly ] + steps: + - run: sudo apt install libwayland-cursor0 libxkbcommon-dev libwayland-dev + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - run: RUSTFLAGS="--cfg loom" cargo test --test concurrency_tests --release + + miri: + name: Miri + runs-on: ubuntu-latest + strategy: + matrix: + features: [ --no-default-features, --all-features ] + steps: + - run: sudo apt install libwayland-cursor0 libxkbcommon-dev libwayland-dev + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + - run: rustup component add miri + - run: RUST_BACKTRACE=1 MIRIFLAGS="-Zmiri-ignore-leaks -Zmiri-disable-isolation" cargo miri test ${{ matrix.features }} diff --git a/Cargo.toml b/Cargo.toml index befac2e..17078bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,17 +1,21 @@ [package] name = "atom_box" -version = "0.1.3" +version = "0.2.0" edition = "2018" authors = ["John Bell "] license = "MIT OR Apache-2.0" readme = "README.md" -description = "A safe idiomatic Rust implementation of Atmoic Box using hazard pointers" +description = "A safe idiomatic Rust implementation of Atomic Box using hazard pointers" repository = "https://github.com/Johnabell/atom_box.git" -keywords = ["atomic", "harard", "pointers", "AtomicBox"] +keywords = ["atomic", "hazard", "pointers", "AtomicBox"] categories = ["concurrency", "rust-patterns", "memory-management"] +[features] +default = ["std"] +std = [] + [target.'cfg(loom)'.dependencies] loom = "0.5" diff --git a/src/domain/list.rs b/src/domain/list.rs index 40a795c..4b2dc5e 100644 --- a/src/domain/list.rs +++ b/src/domain/list.rs @@ -1,5 +1,6 @@ use crate::macros::conditional_const; use crate::sync::{AtomicIsize, AtomicPtr, Ordering}; +use alloc::boxed::Box; #[derive(Debug)] pub(super) struct LockFreeList { @@ -19,7 +20,7 @@ impl LockFreeList { pub, fn new() -> Self { Self { - head: AtomicPtr::new(std::ptr::null_mut()), + head: AtomicPtr::new(core::ptr::null_mut()), count: AtomicIsize::new(0), } } @@ -28,7 +29,7 @@ impl LockFreeList { pub(super) fn push(&self, value: T) -> *mut Node { let node = Box::into_raw(Box::new(Node { value, - next: AtomicPtr::new(std::ptr::null_mut()), + next: AtomicPtr::new(core::ptr::null_mut()), })); // # Safety @@ -87,6 +88,7 @@ impl Drop for LockFreeList { #[cfg(test)] mod test { use super::*; + use alloc::vec::Vec; #[test] fn test_push() { @@ -146,6 +148,6 @@ mod test { "The list should contain all the values from pushed to it from list2 and the original values from list 1" ); // To avoid dropping the nodes which we moved from list2 to list1 - std::mem::forget(list2); + core::mem::forget(list2); } } diff --git a/src/domain/mod.rs b/src/domain/mod.rs index d696aec..3a3a857 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -32,9 +32,13 @@ mod reclaim_strategy; use crate::macros::conditional_const; use crate::sync::Ordering; +use alloc::boxed::Box; +#[cfg(not(feature = "std"))] +use alloc::collections::BTreeSet as Set; use list::{LockFreeList, Node}; pub use reclaim_strategy::{ReclaimStrategy, TimedCappedSettings}; -use std::collections::HashSet; +#[cfg(feature = "std")] +use std::collections::HashSet as Set; pub(crate) trait Retirable {} @@ -51,7 +55,7 @@ impl Retire { fn new(ptr: *mut T) -> Self { Self { ptr: ptr as *mut usize, - retirable: unsafe { std::mem::transmute(ptr as *mut dyn Retirable) }, + retirable: unsafe { core::mem::transmute(ptr as *mut dyn Retirable) }, } } } @@ -146,7 +150,7 @@ On nightly this will panic if the domain id is equal to the shared domain's id ( /// Value must be associated with this domain. /// Value must be able to live as long as the domain. pub(crate) unsafe fn retire(&self, value: *mut T) { - std::sync::atomic::fence(Ordering::SeqCst); + core::sync::atomic::fence(Ordering::SeqCst); self.retired.push(Retire::new(value)); if self.should_reclaim() { @@ -185,9 +189,9 @@ On nightly this will panic if the domain id is equal to the shared domain's id ( let retired_list = self .retired .head - .swap(std::ptr::null_mut(), Ordering::Acquire); + .swap(core::ptr::null_mut(), Ordering::Acquire); - std::sync::atomic::fence(Ordering::SeqCst); + core::sync::atomic::fence(Ordering::SeqCst); self.retired.count.store(0, Ordering::Release); if retired_list.is_null() { @@ -199,11 +203,11 @@ On nightly this will panic if the domain id is equal to the shared domain's id ( fn reclaim_unguarded( &self, - guarded_ptrs: HashSet<*const usize>, + guarded_ptrs: Set<*const usize>, retired_list: *mut Node, ) -> usize { let mut node_ptr = retired_list; - let mut still_retired = std::ptr::null_mut(); + let mut still_retired = core::ptr::null_mut(); let mut tail_ptr = None; let mut reclaimed = 0; let mut number_remaining = 0; @@ -231,7 +235,7 @@ On nightly this will panic if the domain id is equal to the shared domain's id ( // the pointer has not yet been dropped and has only been placed in the retired // list once. There are currently no other threads looking at the value since it is // no longer protected by any of the hazard pointers. - unsafe { std::ptr::drop_in_place(node.value.retirable) }; + unsafe { core::ptr::drop_in_place(node.value.retirable) }; // # Safety // @@ -246,7 +250,7 @@ On nightly this will panic if the domain id is equal to the shared domain's id ( } if let Some(tail) = tail_ptr { - std::sync::atomic::fence(Ordering::SeqCst); + core::sync::atomic::fence(Ordering::SeqCst); // # Safety // @@ -258,8 +262,8 @@ On nightly this will panic if the domain id is equal to the shared domain's id ( reclaimed } - fn get_guarded_ptrs(&self) -> HashSet<*const usize> { - let mut guarded_ptrs = HashSet::new(); + fn get_guarded_ptrs(&self) -> Set<*const usize> { + let mut guarded_ptrs = Set::new(); let mut node_ptr = self.hazard_ptrs.head.load(Ordering::Acquire); while !node_ptr.is_null() { // # Safety diff --git a/src/domain/reclaim_strategy.rs b/src/domain/reclaim_strategy.rs index b4b66c8..a9bbd4b 100644 --- a/src/domain/reclaim_strategy.rs +++ b/src/domain/reclaim_strategy.rs @@ -1,7 +1,10 @@ use crate::macros::conditional_const; +#[cfg(feature = "std")] use crate::sync::{AtomicU64, Ordering}; -use std::time::Duration; +#[cfg(feature = "std")] +use core::time::Duration; +#[cfg(feature = "std")] const DEFAULT_SYNC_THRESHOLD: Duration = Duration::from_nanos(2000000000); const DEFAULT_RETIERED_THRESHOLD: isize = 1000; const DEFAULT_HAZARD_POINTER_MULTIPLIER: isize = 2; @@ -21,7 +24,7 @@ pub enum ReclaimStrategy { /// certain thresholds. TimedCapped(TimedCappedSettings), - /// Memory reclaimation will only happen when the `reclaim` method on [`crate::domain::Domain`] + /// Memory reclamation will only happen when the `reclaim` method on [`crate::domain::Domain`] /// is called. Manual, } @@ -47,15 +50,36 @@ impl ReclaimStrategy { } /// The particulate settings of the `TimedCapped` reclamation strategy. +/// +/// # Example +/// +/// ``` +/// use atom_box::domain::{ReclaimStrategy, TimedCappedSettings}; +/// use core::time::Duration; +/// +/// const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped( +/// #[cfg(feature = "std")] +/// TimedCappedSettings::default() +/// .with_timeout(Duration::from_nanos(5000000000)) +/// .with_retired_threshold(1000) +/// .with_hazard_pointer_multiplier(3), +/// #[cfg(not(feature = "std"))] +/// TimedCappedSettings::default() +/// .with_retired_threshold(1000) +/// .with_hazard_pointer_multiplier(3), +/// ); #[derive(Debug)] pub struct TimedCappedSettings { + #[cfg(feature = "std")] last_sync_time: AtomicU64, + #[cfg(feature = "std")] sync_timeout: Duration, hazard_pointer_multiplier: isize, retired_threshold: isize, } impl TimedCappedSettings { + #[cfg(feature = "std")] conditional_const!( "Creates a new `TimedCappedSettings`. @@ -72,21 +96,23 @@ the retired items. ``` use atom_box::domain::{ReclaimStrategy, TimedCappedSettings}; -const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped(TimedCappedSettings::new( - std::time::Duration::from_nanos(5000000000), +const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped(TimedCappedSettings::new_with_timeout( + core::time::Duration::from_nanos(5000000000), 1000, 3, )); ``` ", pub, - fn new( + fn new_with_timeout( sync_timeout: Duration, retired_threshold: isize, hazard_pointer_multiplier: isize, ) -> Self { Self { + #[cfg(feature = "std")] last_sync_time: AtomicU64::new(0), + #[cfg(feature = "std")] sync_timeout, retired_threshold, hazard_pointer_multiplier, @@ -94,6 +120,40 @@ const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped(TimedCapp } ); + conditional_const!( + "Creates a new `TimedCappedSettings`. + +# Arguments + +* `retired_threshold` - The threshold after which a retired items should be reclaimed +* 'hazard_pointer_multiplier` - If the number of retired items exceeds the number of hazard +pointers multiplied by `hazard_pointer_multiplier` then an attempt will be made to reclaim +the retired items. + +# Example + +``` +use atom_box::domain::{ReclaimStrategy, TimedCappedSettings}; + +const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped(TimedCappedSettings::new( + 1000, + 3, +)); +``` +", + pub, + fn new(retired_threshold: isize, hazard_pointer_multiplier: isize) -> Self { + Self { + #[cfg(feature = "std")] + last_sync_time: AtomicU64::new(0), + #[cfg(feature = "std")] + sync_timeout: DEFAULT_SYNC_THRESHOLD, + retired_threshold, + hazard_pointer_multiplier, + } + } + ); + fn should_reclaim(&self, hazard_pointer_count: isize, retired_count: isize) -> bool { if retired_count >= self.retired_threshold && retired_count >= hazard_pointer_count * self.hazard_pointer_multiplier @@ -103,8 +163,9 @@ const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped(TimedCapp self.check_sync_time() } + #[cfg(feature = "std")] fn check_sync_time(&self) -> bool { - use std::convert::TryFrom; + use core::convert::TryFrom; let time = u64::try_from( std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -127,17 +188,56 @@ const RECLAIM_STRATEGY: ReclaimStrategy = ReclaimStrategy::TimedCapped(TimedCapp .is_ok() } + #[cfg(not(feature = "std"))] + #[inline(always)] + fn check_sync_time(&self) -> bool { + true + } + conditional_const!( -"Creates the default `TimedCappedSettings`. + "Creates the default `TimedCappedSettings`. This is not an implementation of `Default` since it is a const function.", - pub(self), + pub, fn default() -> Self { Self::new( - DEFAULT_SYNC_THRESHOLD, DEFAULT_RETIERED_THRESHOLD, DEFAULT_HAZARD_POINTER_MULTIPLIER, ) } ); + + #[cfg(feature = "std")] + /// Set the timeout after which a reclamation should be attempted. + /// + /// If the time between the previous reclaimation and now exceeds this threshold, an attempt + /// will be made to reclaim the retired items. + pub const fn with_timeout(self, sync_timeout: Duration) -> Self { + Self { + sync_timeout, + ..self + } + } + + /// Set the hazard pointer multiplier. + /// + /// If the number of retired items exceeds the number of hazard pointers multiplied by + /// `hazard_pointer_multiplier` then an attempt will be made to reclaim the retired items. + pub const fn with_hazard_pointer_multiplier(self, hazard_pointer_multiplier: isize) -> Self { + Self { + hazard_pointer_multiplier, + ..self + } + } + + /// Sets the retired threshold. + /// + /// If the number of retired items exceeds this threshold an attempt will be made to reclaim + /// the retired items. + pub const fn with_retired_threshold(self, retired_threshold: isize) -> Self { + Self { + retired_threshold, + ..self + } + } } diff --git a/src/hazard_ptr.rs b/src/hazard_ptr.rs index 3014711..9b3315d 100644 --- a/src/hazard_ptr.rs +++ b/src/hazard_ptr.rs @@ -9,13 +9,13 @@ pub struct HazPtr { impl HazPtr { pub(crate) fn new(active: bool) -> Self { Self { - ptr: AtomicPtr::new(std::ptr::null_mut()), + ptr: AtomicPtr::new(core::ptr::null_mut()), active: AtomicBool::new(active), } } pub(crate) fn reset(&self) { - self.ptr.store(std::ptr::null_mut(), Ordering::Release); + self.ptr.store(core::ptr::null_mut(), Ordering::Release); } pub(crate) fn protect(&self, ptr: *mut usize) { diff --git a/src/lib.rs b/src/lib.rs index 3849c52..5eba90e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,15 +47,20 @@ //! handle2.join().unwrap(); //! ``` +#![no_std] #![warn(missing_docs)] +extern crate alloc; +#[cfg(feature = "std")] +extern crate std; use crate::sync::{AtomicPtr, Ordering}; -use std::ops::Deref; +use core::ops::Deref; pub mod domain; mod hazard_ptr; mod sync; use crate::domain::Domain; +use alloc::boxed::Box; use hazard_ptr::HazPtr; #[cfg(not(loom))] @@ -233,7 +238,7 @@ impl<'domain, T, const DOMAIN_ID: usize> AtomBox<'domain, T, DOMAIN_ID> { // protect pointer haz_ptr.protect(original_ptr as *mut usize); - std::sync::atomic::fence(Ordering::SeqCst); + core::sync::atomic::fence(Ordering::SeqCst); // check pointer let current_ptr = self.ptr.load(Ordering::Acquire); @@ -360,12 +365,12 @@ impl<'domain, T, const DOMAIN_ID: usize> AtomBox<'domain, T, DOMAIN_ID> { new_value: StoreGuard<'domain, T, DOMAIN_ID>, ) -> StoreGuard<'domain, T, DOMAIN_ID> { assert!( - std::ptr::eq(new_value.domain, self.domain), + core::ptr::eq(new_value.domain, self.domain), "Cannot use guarded value from different domain" ); let new_ptr = new_value.ptr; - std::mem::forget(new_value); + core::mem::forget(new_value); let old_ptr = self.ptr.swap(new_ptr as *mut T, Ordering::AcqRel); StoreGuard { ptr: old_ptr, @@ -499,7 +504,7 @@ impl<'domain, T, const DOMAIN_ID: usize> AtomBox<'domain, T, DOMAIN_ID> { ), > { assert!( - std::ptr::eq(new_value.domain, self.domain), + core::ptr::eq(new_value.domain, self.domain), "Cannot use guarded value from different domain" ); @@ -511,7 +516,7 @@ impl<'domain, T, const DOMAIN_ID: usize> AtomBox<'domain, T, DOMAIN_ID> { Ordering::Acquire, ) { Ok(ptr) => { - std::mem::forget(new_value); + core::mem::forget(new_value); Ok(StoreGuard { ptr, domain: self.domain, @@ -654,7 +659,7 @@ impl<'domain, T, const DOMAIN_ID: usize> AtomBox<'domain, T, DOMAIN_ID> { ), > { assert!( - std::ptr::eq(new_value.domain, self.domain), + core::ptr::eq(new_value.domain, self.domain), "Cannot use guarded value from different domain" ); @@ -666,7 +671,7 @@ impl<'domain, T, const DOMAIN_ID: usize> AtomBox<'domain, T, DOMAIN_ID> { Ordering::Acquire, ) { Ok(ptr) => { - std::mem::forget(new_value); + core::mem::forget(new_value); Ok(StoreGuard { ptr, domain: self.domain, @@ -697,7 +702,7 @@ impl<'domain, T, const DOMAIN_ID: usize> Drop for AtomBox<'domain, T, DOMAIN_ID> // We are safe to flag it for retire, where it will be reclaimed when it is no longer // protected by any hazard pointers. let ptr = self.ptr.load(Ordering::Relaxed); - unsafe { self.domain.retire(ptr as *mut T) }; + unsafe { self.domain.retire(ptr) }; } } @@ -781,7 +786,7 @@ impl Deref for LoadGuard<'_, T, DOMAIN_ID> { mod test { use super::*; - pub(crate) use std::sync::atomic::AtomicUsize; + pub(crate) use core::sync::atomic::AtomicUsize; static TEST_DOMAIN: domain::Domain<1> = Domain::new(domain::ReclaimStrategy::Eager); diff --git a/src/sync.rs b/src/sync.rs index 9854464..f9b7e50 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,7 +1,9 @@ #[cfg(loom)] pub(crate) use loom::sync::atomic::{AtomicBool, AtomicIsize, AtomicPtr, AtomicU64}; +#[cfg(all(feature = "std", not(loom)))] +pub(crate) use core::sync::atomic::AtomicU64; #[cfg(not(loom))] -pub(crate) use std::sync::atomic::{AtomicBool, AtomicIsize, AtomicPtr, AtomicU64}; +pub(crate) use core::sync::atomic::{AtomicBool, AtomicIsize, AtomicPtr}; -pub(crate) use std::sync::atomic::Ordering; +pub(crate) use core::sync::atomic::Ordering;