diff --git a/CHANGELOG.md b/CHANGELOG.md index de5e795..e656983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 1.5.0 + +- add new `once_cell::race` module for "first one wins" no_std-compatible initialization flavor. + The API is provisional, subject to change and is gated by the `unstable` cargo feature. + ## 1.4.1 - upgrade `parking_lot` to `0.11.0` diff --git a/Cargo.toml b/Cargo.toml index 2a53697..d80baa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "once_cell" -version = "1.4.1" +version = "1.5.0" authors = ["Aleksey Kladov "] license = "MIT OR Apache-2.0" edition = "2018" @@ -33,6 +33,8 @@ regex = "1.2.0" default = ["std"] # Enables `once_cell::sync` module. std = [] +# Enables semver-exempt APIs of this crate +unstable = [] [[example]] name = "bench" @@ -61,3 +63,6 @@ required-features = ["std"] [[example]] name = "test_synchronization" required-features = ["std"] + +[package.metadata.docs.rs] +all-features = true diff --git a/src/lib.rs b/src/lib.rs index 9ae680f..119de86 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1039,3 +1039,196 @@ pub mod sync { /// ``` fn _dummy() {} } + +/// "First one wins" flavor of `OnceCell`. +/// +/// If two threads race to initialize a type from the `race` module, they +/// don't block, execute initialization function together, but only one of +/// them stores the result. +/// +/// This module does not require `std` feature. +#[cfg(feature = "unstable")] +pub mod race { + use core::{ + num::NonZeroUsize, + sync::atomic::{AtomicUsize, Ordering}, + }; + #[cfg(feature = "std")] + use std::{marker::PhantomData, ptr, sync::atomic::AtomicPtr}; + + #[derive(Default, Debug)] + pub struct OnceNonZeroUsize { + inner: AtomicUsize, + } + + impl OnceNonZeroUsize { + pub const fn new() -> OnceNonZeroUsize { + OnceNonZeroUsize { inner: AtomicUsize::new(0) } + } + + pub fn get(&self) -> Option { + let val = self.inner.load(Ordering::Acquire); + NonZeroUsize::new(val) + } + + pub fn set(&self, value: NonZeroUsize) -> Result<(), ()> { + let val = self.inner.compare_and_swap(0, value.get(), Ordering::AcqRel); + if val == 0 { + Ok(()) + } else { + Err(()) + } + } + + pub fn get_or_init(&self, f: F) -> NonZeroUsize + where + F: FnOnce() -> NonZeroUsize, + { + enum Void {} + match self.get_or_try_init(|| Ok::(f())) { + Ok(val) => val, + Err(void) => match void {}, + } + } + + pub fn get_or_try_init(&self, f: F) -> Result + where + F: FnOnce() -> Result, + { + let val = self.inner.load(Ordering::Acquire); + let res = match NonZeroUsize::new(val) { + Some(it) => it, + None => { + let mut val = f()?.get(); + let old_val = self.inner.compare_and_swap(0, val, Ordering::AcqRel); + if old_val != 0 { + val = old_val; + } + unsafe { NonZeroUsize::new_unchecked(val) } + } + }; + Ok(res) + } + } + + #[derive(Default, Debug)] + pub struct OnceBool { + inner: OnceNonZeroUsize, + } + + impl OnceBool { + fn from_usize(value: NonZeroUsize) -> bool { + value.get() == 1 + } + fn to_usize(value: bool) -> NonZeroUsize { + unsafe { NonZeroUsize::new_unchecked(if value { 1 } else { 2 }) } + } + + pub const fn new() -> OnceBool { + OnceBool { inner: OnceNonZeroUsize::new() } + } + + pub fn get(&self) -> Option { + self.inner.get().map(OnceBool::from_usize) + } + + pub fn set(&self, value: bool) -> Result<(), ()> { + self.inner.set(OnceBool::to_usize(value)) + } + + pub fn get_or_init(&self, f: F) -> bool + where + F: FnOnce() -> bool, + { + OnceBool::from_usize(self.inner.get_or_init(|| OnceBool::to_usize(f()))) + } + + pub fn get_or_try_init(&self, f: F) -> Result + where + F: FnOnce() -> Result, + { + self.inner.get_or_try_init(|| f().map(OnceBool::to_usize)).map(OnceBool::from_usize) + } + } + + #[derive(Default, Debug)] + #[cfg(feature = "std")] + pub struct OnceBox { + inner: AtomicPtr, + ghost: PhantomData>>, + } + + #[cfg(feature = "std")] + impl Drop for OnceBox { + fn drop(&mut self) { + let ptr = *self.inner.get_mut(); + if !ptr.is_null() { + drop(unsafe { Box::from_raw(ptr) }) + } + } + } + + #[cfg(feature = "std")] + impl OnceBox { + pub const fn new() -> OnceBox { + OnceBox { inner: AtomicPtr::new(ptr::null_mut()), ghost: PhantomData } + } + + pub fn get(&self) -> Option<&T> { + let ptr = self.inner.load(Ordering::Acquire); + if ptr.is_null() { + return None; + } + Some(unsafe { &*ptr }) + } + + // Result<(), Box> here? + pub fn set(&self, value: T) -> Result<(), ()> { + let ptr = Box::into_raw(Box::new(value)); + if ptr.is_null() { + drop(unsafe { Box::from_raw(ptr) }); + return Err(()); + } + Ok(()) + } + + pub fn get_or_init(&self, f: F) -> &T + where + F: FnOnce() -> T, + { + enum Void {} + match self.get_or_try_init(|| Ok::(f())) { + Ok(val) => val, + Err(void) => match void {}, + } + } + + pub fn get_or_try_init(&self, f: F) -> Result<&T, E> + where + F: FnOnce() -> Result, + { + let mut ptr = self.inner.load(Ordering::Acquire); + + if ptr.is_null() { + let val = f()?; + ptr = Box::into_raw(Box::new(val)); + let old_ptr = self.inner.compare_and_swap(ptr::null_mut(), ptr, Ordering::AcqRel); + if !old_ptr.is_null() { + drop(unsafe { Box::from_raw(ptr) }); + ptr = old_ptr; + } + }; + Ok(unsafe { &*ptr }) + } + } + + /// ```compile_fail + /// struct S(*mut ()); + /// unsafe impl Sync for S {} + /// + /// fn share(_: &T) {} + /// share(&once_cell::race::OnceBox::::new()); + /// ``` + #[cfg(feature = "std")] + unsafe impl Sync for OnceBox {} +} diff --git a/tests/it.rs b/tests/it.rs index f692615..05025e5 100644 --- a/tests/it.rs +++ b/tests/it.rs @@ -570,3 +570,216 @@ mod sync { } } } + +#[cfg(feature = "unstable")] +mod race { + use std::{ + num::NonZeroUsize, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Barrier, + }, + }; + + use crossbeam_utils::thread::scope; + + use once_cell::race::{OnceBool, OnceNonZeroUsize}; + + #[test] + fn once_non_zero_usize_smoke_test() { + let cnt = AtomicUsize::new(0); + let cell = OnceNonZeroUsize::new(); + let val = NonZeroUsize::new(92).unwrap(); + scope(|s| { + s.spawn(|_| { + assert_eq!( + cell.get_or_init(|| { + cnt.fetch_add(1, SeqCst); + val + }), + val + ); + assert_eq!(cnt.load(SeqCst), 1); + + assert_eq!( + cell.get_or_init(|| { + cnt.fetch_add(1, SeqCst); + val + }), + val + ); + assert_eq!(cnt.load(SeqCst), 1); + }); + }) + .unwrap(); + assert_eq!(cell.get(), Some(val)); + assert_eq!(cnt.load(SeqCst), 1); + } + + #[test] + fn once_non_zero_usize_first_wins() { + let cell = OnceNonZeroUsize::new(); + let val1 = NonZeroUsize::new(92).unwrap(); + let val2 = NonZeroUsize::new(62).unwrap(); + + let b1 = Barrier::new(2); + let b2 = Barrier::new(2); + let b3 = Barrier::new(2); + scope(|s| { + s.spawn(|_| { + let r1 = cell.get_or_init(|| { + b1.wait(); + b2.wait(); + val1 + }); + assert_eq!(r1, val1); + b3.wait(); + }); + b1.wait(); + s.spawn(|_| { + let r2 = cell.get_or_init(|| { + b2.wait(); + b3.wait(); + val2 + }); + assert_eq!(r2, val1); + }); + }) + .unwrap(); + + assert_eq!(cell.get(), Some(val1)); + } + + #[test] + fn once_bool_smoke_test() { + let cnt = AtomicUsize::new(0); + let cell = OnceBool::new(); + scope(|s| { + s.spawn(|_| { + assert_eq!( + cell.get_or_init(|| { + cnt.fetch_add(1, SeqCst); + false + }), + false + ); + assert_eq!(cnt.load(SeqCst), 1); + + assert_eq!( + cell.get_or_init(|| { + cnt.fetch_add(1, SeqCst); + false + }), + false + ); + assert_eq!(cnt.load(SeqCst), 1); + }); + }) + .unwrap(); + assert_eq!(cell.get(), Some(false)); + assert_eq!(cnt.load(SeqCst), 1); + } + + #[test] + #[cfg(feature = "std")] + fn once_box_smoke_test() { + #[derive(Debug)] + struct Pebble { + id: usize, + } + static TOTAL: AtomicUsize = AtomicUsize::new(0); + + impl Pebble { + fn total() -> usize { + TOTAL.load(SeqCst) + } + fn new() -> Pebble { + let id = TOTAL.fetch_add(1, SeqCst); + Pebble { id } + } + } + impl Drop for Pebble { + fn drop(&mut self) { + TOTAL.fetch_sub(1, SeqCst); + } + } + + let global_cnt = AtomicUsize::new(0); + let cell = once_cell::race::OnceBox::new(); + let b = Barrier::new(128); + scope(|s| { + for _ in 0..128 { + s.spawn(|_| { + let local_cnt = AtomicUsize::new(0); + cell.get_or_init(|| { + global_cnt.fetch_add(1, SeqCst); + local_cnt.fetch_add(1, SeqCst); + b.wait(); + Pebble::new() + }); + assert_eq!(local_cnt.load(SeqCst), 1); + + cell.get_or_init(|| { + global_cnt.fetch_add(1, SeqCst); + local_cnt.fetch_add(1, SeqCst); + Pebble::new() + }); + assert_eq!(local_cnt.load(SeqCst), 1); + }); + } + }) + .unwrap(); + assert!(cell.get().is_some()); + assert!(global_cnt.load(SeqCst) > 10); + + assert_eq!(Pebble::total(), 1); + drop(cell); + assert_eq!(Pebble::total(), 0); + } + + #[test] + #[cfg(feature = "std")] + fn once_box_first_wins() { + let cell = once_cell::race::OnceBox::new(); + let val1 = 92; + let val2 = 62; + + let b1 = Barrier::new(2); + let b2 = Barrier::new(2); + let b3 = Barrier::new(2); + scope(|s| { + s.spawn(|_| { + let r1 = cell.get_or_init(|| { + b1.wait(); + b2.wait(); + val1 + }); + assert_eq!(*r1, val1); + b3.wait(); + }); + b1.wait(); + s.spawn(|_| { + let r2 = cell.get_or_init(|| { + b2.wait(); + b3.wait(); + val2 + }); + assert_eq!(*r2, val1); + }); + }) + .unwrap(); + + assert_eq!(cell.get(), Some(&val1)); + } + + #[test] + #[cfg(feature = "std")] + fn once_box_reentrant() { + let cell = once_cell::race::OnceBox::new(); + let res = cell.get_or_init(|| { + cell.get_or_init(|| "hello".to_string()); + "world".to_string() + }); + assert_eq!(res, "hello"); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 85c903f..7d7bac3 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -24,21 +24,21 @@ fn try_main() -> Result<()> { { let _s = section("TEST_STABLE"); let _t = push_rustup_toolchain("stable"); - cmd!("cargo test").run()?; - cmd!("cargo test --release").run()?; + cmd!("cargo test --features unstable").run()?; + cmd!("cargo test --features unstable --release").run()?; // Skip doctests, they need `std` - cmd!("cargo test --no-default-features --test it").run()?; + cmd!("cargo test --features unstable --no-default-features --test it").run()?; - cmd!("cargo test --no-default-features --features 'std parking_lot'").run()?; - cmd!("cargo test --no-default-features --features 'std parking_lot' --release").run()?; + cmd!("cargo test --features unstable --no-default-features --features 'std parking_lot'").run()?; + cmd!("cargo test --features unstable --no-default-features --features 'std parking_lot' --release").run()?; } { let _s = section("TEST_BETA"); let _t = push_rustup_toolchain("beta"); - cmd!("cargo test").run()?; - cmd!("cargo test --release").run()?; + cmd!("cargo test --features unstable").run()?; + cmd!("cargo test --features unstable --release").run()?; } { @@ -57,7 +57,7 @@ fn try_main() -> Result<()> { cmd!("rustup component add miri").run()?; cmd!("cargo miri setup").run()?; - cmd!("cargo miri test").run()?; + cmd!("cargo miri test --features unstable").run()?; } let version = cargo_toml.version()?;