From a13a2d0a30de7e067c85ebacbddf6f20ed437d32 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sat, 26 Jul 2025 13:01:52 +0200 Subject: [PATCH 1/3] add `nonpoison::once` implementation Adds the equivalent `nonpoison` types to the `poison::once` module. These types and implementations are gated under the `nonpoison_once` feature gate. --- library/std/src/sync/nonpoison.rs | 3 + library/std/src/sync/nonpoison/once.rs | 221 +++++++++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 library/std/src/sync/nonpoison/once.rs diff --git a/library/std/src/sync/nonpoison.rs b/library/std/src/sync/nonpoison.rs index 2bbf226dc2cde..f7127b327e82f 100644 --- a/library/std/src/sync/nonpoison.rs +++ b/library/std/src/sync/nonpoison.rs @@ -33,5 +33,8 @@ impl fmt::Display for WouldBlock { pub use self::mutex::MappedMutexGuard; #[unstable(feature = "nonpoison_mutex", issue = "134645")] pub use self::mutex::{Mutex, MutexGuard}; +#[unstable(feature = "nonpoison_once", issue = "134645")] +pub use self::once::Once; mod mutex; +pub(crate) mod once; diff --git a/library/std/src/sync/nonpoison/once.rs b/library/std/src/sync/nonpoison/once.rs new file mode 100644 index 0000000000000..563c73cec4dd9 --- /dev/null +++ b/library/std/src/sync/nonpoison/once.rs @@ -0,0 +1,221 @@ +use crate::fmt; +use crate::sys::sync as sys; + +/// A low-level synchronization primitive for one-time global execution that ignores poisoning. +/// +/// For more information about `Once`, check out the documentation for the poisoning variant (which +/// can be found at [`poison::Once`]). +/// +/// [`poison::Once`]: crate::sync::poison::Once +/// +/// # Examples +/// +/// ``` +/// use std::sync::Once; +/// +/// static START: Once = Once::new(); +/// +/// START.call_once(|| { +/// // run initialization here +/// }); +/// ``` +#[unstable(feature = "nonpoison_once", issue = "134645")] +pub struct Once { + inner: sys::Once, +} + +impl Once { + /// Creates a new `Once` value. + #[inline] + #[unstable(feature = "nonpoison_once", issue = "134645")] + #[must_use] + pub const fn new() -> Once { + Once { inner: sys::Once::new() } + } + + /// Performs an initialization routine once and only once. The given closure + /// will be executed if this is the first time `call_once` has been called, + /// and otherwise the routine will *not* be invoked. + /// + /// This method will block the calling thread if another initialization + /// routine is currently running. + /// + /// When this function returns, it is guaranteed that some initialization + /// has run and completed (it might not be the closure specified). It is also + /// guaranteed that any memory writes performed by the executed closure can + /// be reliably observed by other threads at this point (there is a + /// happens-before relation between the closure and code executing after the + /// return). + /// + /// If the given closure recursively invokes `call_once` on the same [`Once`] + /// instance, the exact behavior is not specified: allowed outcomes are + /// a panic or a deadlock. + /// + /// # Examples + /// + /// ``` + /// #![feature(nonpoison_once)] + /// + /// use std::sync::nonpoison::Once; + /// + /// static mut VAL: usize = 0; + /// static INIT: Once = Once::new(); + /// + /// // Accessing a `static mut` is unsafe much of the time, but if we do so + /// // in a synchronized fashion (e.g., write once or read all) then we're + /// // good to go! + /// // + /// // This function will only call `expensive_computation` once, and will + /// // otherwise always return the value returned from the first invocation. + /// fn get_cached_val() -> usize { + /// unsafe { + /// INIT.call_once(|| { + /// VAL = expensive_computation(); + /// }); + /// VAL + /// } + /// } + /// + /// fn expensive_computation() -> usize { + /// // ... + /// # 2 + /// } + /// ``` + /// + /// # Panics + /// + /// The closure `f` will only be executed once even if this is called concurrently amongst many + /// threads. If the closure panics, the calling thread will panic and the `Once` will remain in + /// an incompleted state. + /// + /// In contrast to the [`poison::Once`] variant, all calls to `call_once` will ignore panics in + /// other threads. This method is identical to the [`poison::Once::call_once_force`] method. + /// + /// If you need observability into whether any threads have panicked while calling `call_once`, + /// see [`poison::Once`]. + /// + /// [`poison::Once`]: crate::sync::poison::Once + /// [`poison::Once::call_once_force`]: crate::sync::poison::Once::call_once_force + /// + /// ``` + /// #![feature(nonpoison_once)] + /// + /// use std::sync::nonpoison::Once; + /// use std::thread; + /// + /// static INIT: Once = Once::new(); + /// + /// // Panic during `call_once`. + /// let handle = thread::spawn(|| { + /// INIT.call_once(|| panic!()); + /// }); + /// assert!(handle.join().is_err()); + /// + /// // `call_once` will still run from a different thread. + /// INIT.call_once(|| { + /// assert_eq!(2 + 2, 4); + /// }); + /// ``` + #[inline] + #[unstable(feature = "nonpoison_once", issue = "134645")] + #[track_caller] + pub fn call_once(&self, f: F) + where + F: FnOnce(), + { + // Fast path check. + if self.inner.is_completed() { + return; + } + + let mut f = Some(f); + self.inner.call(true, &mut |_| f.take().unwrap()()); + } + + /// Returns `true` if some [`call_once()`] call has completed + /// successfully. Specifically, `is_completed` will return false in + /// the following situations: + /// * [`call_once()`] was not called at all, + /// * [`call_once()`] was called, but has not yet completed + /// + /// This function returning `false` does not mean that [`Once`] has not been + /// executed. For example, it may have been executed in the time between + /// when `is_completed` starts executing and when it returns, in which case + /// the `false` return value would be stale (but still permissible). + /// + /// [`call_once()`]: Once::call_once + /// + /// # Examples + /// + /// ``` + /// #![feature(nonpoison_once)] + /// + /// use std::sync::nonpoison::Once; + /// + /// static INIT: Once = Once::new(); + /// + /// assert_eq!(INIT.is_completed(), false); + /// INIT.call_once(|| { + /// assert_eq!(INIT.is_completed(), false); + /// }); + /// assert_eq!(INIT.is_completed(), true); + /// ``` + /// + /// ``` + /// #![feature(nonpoison_once)] + /// + /// use std::sync::nonpoison::Once; + /// use std::thread; + /// + /// static INIT: Once = Once::new(); + /// + /// assert_eq!(INIT.is_completed(), false); + /// let handle = thread::spawn(|| { + /// INIT.call_once(|| panic!()); + /// }); + /// assert!(handle.join().is_err()); + /// assert_eq!(INIT.is_completed(), false); + /// ``` + #[inline] + #[unstable(feature = "nonpoison_once", issue = "134645")] + pub fn is_completed(&self) -> bool { + self.inner.is_completed() + } + + /// Blocks the current thread until initialization has completed. + /// + /// # Example + /// + /// ```rust + /// use std::sync::Once; + /// use std::thread; + /// + /// static READY: Once = Once::new(); + /// + /// let thread = thread::spawn(|| { + /// READY.wait(); + /// println!("everything is ready"); + /// }); + /// + /// READY.call_once(|| println!("performing setup")); + /// ``` + /// + /// This function will continue to block even if a thread initializing via [`call_once()`] has + /// panicked. This behavior is identical to the [`poison::Once::wait_force`] method. + /// + /// [`call_once()`]: Once::call_once + /// [`poison::Once::wait_force`]: crate::sync::poison::Once::wait_force + #[unstable(feature = "nonpoison_once", issue = "134645")] + pub fn wait(&self) { + if !self.inner.is_completed() { + self.inner.wait(true); + } + } +} + +#[unstable(feature = "nonpoison_once", issue = "134645")] +impl fmt::Debug for Once { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Once").finish_non_exhaustive() + } +} From 94588d9ce519067806937c3d7c74a53095e5a68e Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sun, 27 Jul 2025 22:11:15 +0200 Subject: [PATCH 2/3] add nonpoison and poison once tests --- library/std/tests/sync/lib.rs | 1 + library/std/tests/sync/once.rs | 136 ++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 61 deletions(-) diff --git a/library/std/tests/sync/lib.rs b/library/std/tests/sync/lib.rs index 94f1fe96b6a26..3e9cbcb52fe94 100644 --- a/library/std/tests/sync/lib.rs +++ b/library/std/tests/sync/lib.rs @@ -8,6 +8,7 @@ #![feature(std_internals)] #![feature(sync_nonpoison)] #![feature(nonpoison_mutex)] +#![feature(nonpoison_once)] #![allow(internal_features)] #![feature(macro_metavar_expr_concat)] // For concatenating identifiers in macros. diff --git a/library/std/tests/sync/once.rs b/library/std/tests/sync/once.rs index 1b43831df3a4b..9492d50a3d298 100644 --- a/library/std/tests/sync/once.rs +++ b/library/std/tests/sync/once.rs @@ -5,51 +5,84 @@ use std::sync::mpsc::channel; use std::time::Duration; use std::{panic, thread}; -#[test] -fn smoke_once() { - static O: Once = Once::new(); - let mut a = 0; - O.call_once(|| a += 1); - assert_eq!(a, 1); - O.call_once(|| a += 1); - assert_eq!(a, 1); -} - -#[test] -fn stampede_once() { - static O: Once = Once::new(); - static mut RUN: bool = false; - - let (tx, rx) = channel(); - for _ in 0..10 { - let tx = tx.clone(); - thread::spawn(move || { - for _ in 0..4 { - thread::yield_now() - } - unsafe { - O.call_once(|| { - assert!(!RUN); - RUN = true; - }); - assert!(RUN); - } - tx.send(()).unwrap(); - }); +use super::nonpoison_and_poison_unwrap_test; + +nonpoison_and_poison_unwrap_test!( + name: smoke_once, + test_body: { + use locks::Once; + + static O: Once = Once::new(); + let mut a = 0; + O.call_once(|| a += 1); + assert_eq!(a, 1); + O.call_once(|| a += 1); + assert_eq!(a, 1); } - - unsafe { - O.call_once(|| { - assert!(!RUN); - RUN = true; - }); - assert!(RUN); +); + +nonpoison_and_poison_unwrap_test!( + name: stampede_once, + test_body: { + use locks::Once; + + static O: Once = Once::new(); + static mut RUN: bool = false; + + let (tx, rx) = channel(); + for _ in 0..10 { + let tx = tx.clone(); + thread::spawn(move || { + for _ in 0..4 { + thread::yield_now() + } + unsafe { + O.call_once(|| { + assert!(!RUN); + RUN = true; + }); + assert!(RUN); + } + tx.send(()).unwrap(); + }); + } + + unsafe { + O.call_once(|| { + assert!(!RUN); + RUN = true; + }); + assert!(RUN); + } + + for _ in 0..10 { + rx.recv().unwrap(); + } } - - for _ in 0..10 { - rx.recv().unwrap(); +); + +nonpoison_and_poison_unwrap_test!( + name: wait, + test_body: { + use locks::Once; + + for _ in 0..50 { + let val = AtomicBool::new(false); + let once = Once::new(); + + thread::scope(|s| { + for _ in 0..4 { + s.spawn(|| { + once.wait(); + assert!(val.load(Relaxed)); + }); + } + + once.call_once(|| val.store(true, Relaxed)); + }); + } } -} +); #[test] #[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")] @@ -119,25 +152,6 @@ fn wait_for_force_to_finish() { assert!(t2.join().is_ok()); } -#[test] -fn wait() { - for _ in 0..50 { - let val = AtomicBool::new(false); - let once = Once::new(); - - thread::scope(|s| { - for _ in 0..4 { - s.spawn(|| { - once.wait(); - assert!(val.load(Relaxed)); - }); - } - - once.call_once(|| val.store(true, Relaxed)); - }); - } -} - #[test] #[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")] fn wait_on_poisoned() { From b201b4245fdc3bc789af3887ce42257b05f84309 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sat, 26 Jul 2025 13:06:07 +0200 Subject: [PATCH 3/3] remove FIXMEs in `once_lock` and `lazy_lock` Both implementations require poisoning support. --- library/std/src/sync/lazy_lock.rs | 2 +- library/std/src/sync/once_lock.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/library/std/src/sync/lazy_lock.rs b/library/std/src/sync/lazy_lock.rs index eba849d16dacd..13bf9a0a5f9c8 100644 --- a/library/std/src/sync/lazy_lock.rs +++ b/library/std/src/sync/lazy_lock.rs @@ -63,7 +63,7 @@ union Data { /// ``` #[stable(feature = "lazy_cell", since = "1.80.0")] pub struct LazyLock T> { - // FIXME(nonpoison_once): if possible, switch to nonpoison version once it is available + /// We use `poison::Once` here to enable the `force_mut` method. once: Once, data: UnsafeCell>, } diff --git a/library/std/src/sync/once_lock.rs b/library/std/src/sync/once_lock.rs index a5c3a6c46a433..cdfe4d06a2fd0 100644 --- a/library/std/src/sync/once_lock.rs +++ b/library/std/src/sync/once_lock.rs @@ -104,9 +104,10 @@ use crate::sync::Once; /// ``` #[stable(feature = "once_cell", since = "1.70.0")] pub struct OnceLock { - // FIXME(nonpoison_once): switch to nonpoison version once it is available + /// We use `poison::Once` here to allow us to pseudo-"poison" the `Once` whenever a + /// `get_or_try_init` fails, which allows other calls to be run after a failure. once: Once, - // Whether or not the value is initialized is tracked by `once.is_completed()`. + /// Note that `once.is_completed()` tells us if the value is initialized or not. value: UnsafeCell>, /// `PhantomData` to make sure dropck understands we're dropping T in our Drop impl. ///