You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@asyncband.apache.org by ti...@apache.org on 2026/08/26 04:51:14 UTC
(asyncband) branch main updated: feat(once): Add async LazyCell implementation (#168)
This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/asyncband.git
The following commit(s) were added to refs/heads/main by this push:
new 3314e99 feat(once): Add async LazyCell implementation (#168)
3314e99 is described below
commit 3314e996e225fef52e53f710dcb20d758bc6b79d
Author: Matt <14...@users.noreply.github.com>
AuthorDate: Wed Aug 26 00:51:10 2026 -0400
feat(once): Add async LazyCell implementation (#168)
Co-authored-by: tison <wa...@gmail.com>
---
AGENTS.md | 20 ++
CHANGELOG.md | 1 +
Cargo.lock | 8 +
Cargo.toml | 2 +-
README.md | 9 +
asyncband/Cargo.toml | 1 +
asyncband/src/internal/mod.rs | 10 +-
asyncband/src/internal/value_cell.rs | 142 +++++++++++++
asyncband/src/lib.rs | 9 +-
asyncband/src/once/lazy_cell/mod.rs | 273 +++++++++++++++++++++++++
asyncband/src/once/mod.rs | 4 +
asyncband/src/once/once_cell/mod.rs | 105 +++-------
{tests-integration => examples}/Cargo.toml | 40 +---
examples/src/once_cell_vs_lazy_cell.rs | 129 ++++++++++++
tests-integration/Cargo.toml | 1 +
tests-integration/tests/lazy_cell_test.rs | 309 +++++++++++++++++++++++++++++
tests-integration/tests/traits_test.rs | 3 +
17 files changed, 953 insertions(+), 113 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..78d5903
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,20 @@
+# Agent Instructions
+
+## Cargo XTask
+
+Use `cargo x` as the source of truth for repository workflows.
+
+- Run `cargo x --help` before choosing build, test, lint, or formatting commands.
+- Run `cargo x <command> --help` for command-specific behavior.
+
+## Code Style
+
+- Express visibility at the module boundary. Inside a restricted module, use `pub` for its module API instead of repeating `pub(crate)`.
+
+## Commits and Pull Requests
+
+Follow the semantic definition at `.github/semantic.yml`.
+
+- Keep title descriptions short.
+- Simple PR descriptions should only include a `Summary` section.
+- Complex PR descriptions may also include a `Design Notes` section.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0d21b9..ef6ca49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file.
* Add an opt-in `asyncband::blocking::FutureExt` bridge with `block_on` and `wait_timeout` methods for waiting on runtime-agnostic futures from synchronous code.
* Add opt-in bounded and unbounded runtime-agnostic object pools under `asyncband::pool`.
+* Add opt-in `asyncband::once::LazyCell` for values that own one asynchronous initializer and preserve its in-flight future across caller cancellation.
### Breaking changes
diff --git a/Cargo.lock b/Cargo.lock
index c72436f..4a150d3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -283,6 +283,14 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "examples"
+version = "0.0.0"
+dependencies = [
+ "asyncband",
+ "tokio",
+]
+
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
diff --git a/Cargo.toml b/Cargo.toml
index 4dde95d..2cc043c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -16,7 +16,7 @@
# under the License.
[workspace]
-members = ["asyncband", "benchmarks", "tests-integration", "xtask"]
+members = ["asyncband", "benchmarks", "examples", "tests-integration", "xtask"]
resolver = "3"
[workspace.package]
diff --git a/README.md b/README.md
index 44ecbb6..7a8a40b 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,14 @@ async fn increment() {
Public paths stay direct—such as `asyncband::mutex`, `asyncband::pool`, and `asyncband::once::OnceCell`—while Cargo features keep unused implementations out of the build.
+## Examples
+
+Runnable examples live in the [`examples`](examples) workspace crate. For example, the [`OnceCell` versus `LazyCell` initialization guide](examples/src/once_cell_vs_lazy_cell.rs) demonstrates when a restartable fixed free function is sufficient, when initialization needs access-time context and retries, and when a lazy value needs to own and preserve a one-shot initializer.
+
+```shell
+cargo run -p examples --example once_cell_vs_lazy_cell
+```
+
## API map
| Area | API | Feature | Use |
@@ -65,6 +73,7 @@ Public paths stay direct—such as `asyncband::mutex`, `asyncband::pool`, and `a
| | [`Condvar`](https://docs.rs/asyncband/*/asyncband/condvar/struct.Condvar.html) | `condvar` | Wait for notifications while releasing a mutex. |
| Initialization | [`Once`](https://docs.rs/asyncband/*/asyncband/once/struct.Once.html) | `once` | Run asynchronous initialization exactly once. |
| | [`OnceCell`](https://docs.rs/asyncband/*/asyncband/once/struct.OnceCell.html) | `once-cell` | Initialize and store one asynchronous value. |
+| | [`LazyCell`](https://docs.rs/asyncband/*/asyncband/once/struct.LazyCell.html) | `lazy-cell` | Lazily initialize a value with a stored asynchronous function. |
| | [`OnceMap`](https://docs.rs/asyncband/*/asyncband/once/struct.OnceMap.html) | `once-map` | Initialize and store one value per key. |
| Task coordination | [`Barrier`](https://docs.rs/asyncband/*/asyncband/barrier/struct.Barrier.html) | `barrier` | Wait until all participants reach a synchronization point. |
| | [`Latch`](https://docs.rs/asyncband/*/asyncband/latch/struct.Latch.html) | `latch` | Wait until a one-way countdown completes. |
diff --git a/asyncband/Cargo.toml b/asyncband/Cargo.toml
index af8e642..3871529 100644
--- a/asyncband/Cargo.toml
+++ b/asyncband/Cargo.toml
@@ -48,6 +48,7 @@ barrier = []
blocking = []
condvar = ["mutex"]
latch = []
+lazy-cell = ["mutex"]
mpsc = []
mutex = []
once = ["semaphore"]
diff --git a/asyncband/src/internal/mod.rs b/asyncband/src/internal/mod.rs
index 47c1751..cdd33e3 100644
--- a/asyncband/src/internal/mod.rs
+++ b/asyncband/src/internal/mod.rs
@@ -39,9 +39,17 @@ mod arena;
pub(crate) mod countdown;
#[cfg(any(feature = "once-map", feature = "singleflight"))]
-#[cfg_attr(not(feature = "once-map"), allow(dead_code))]
+// `OnceMap` and `singleflight` use different subsets of `OnceTable`, so single-feature builds
+// leave some operations in the shared implementation unused.
+#[allow(dead_code)]
pub(crate) mod once_table;
+#[cfg(any(feature = "lazy-cell", feature = "once-cell"))]
+// `LazyCell` and `OnceCell` use different subsets of `ValueCell`, so single-feature builds leave
+// some operations in the shared implementation unused.
+#[allow(dead_code)]
+pub(crate) mod value_cell;
+
#[cfg(any(
feature = "barrier",
feature = "latch",
diff --git a/asyncband/src/internal/value_cell.rs b/asyncband/src/internal/value_cell.rs
new file mode 100644
index 0000000..928b1b8
--- /dev/null
+++ b/asyncband/src/internal/value_cell.rs
@@ -0,0 +1,142 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::cell::UnsafeCell;
+use std::mem::MaybeUninit;
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::Ordering;
+
+/// Storage for a value published exactly once.
+///
+/// Synchronizing initialization is deliberately left to the containing primitive.
+pub struct ValueCell<T> {
+ initialized: AtomicBool,
+ value: UnsafeCell<MaybeUninit<T>>,
+}
+
+// SAFETY: Shared access only exposes `&T`, and publication is synchronized by the initialized
+// flag. The containing primitive is responsible for serializing writes.
+unsafe impl<T: Sync + Send> Sync for ValueCell<T> {}
+
+// SAFETY: Ownership of the cell and its value may be transferred when `T` is `Send`.
+unsafe impl<T: Send> Send for ValueCell<T> {}
+
+impl<T> ValueCell<T> {
+ pub const fn new() -> Self {
+ Self {
+ initialized: AtomicBool::new(false),
+ value: UnsafeCell::new(MaybeUninit::uninit()),
+ }
+ }
+
+ pub const fn from_value(value: T) -> Self {
+ Self {
+ initialized: AtomicBool::new(true),
+ value: UnsafeCell::new(MaybeUninit::new(value)),
+ }
+ }
+
+ pub fn is_initialized(&self) -> bool {
+ self.initialized.load(Ordering::Acquire)
+ }
+
+ pub fn is_initialized_mut(&mut self) -> bool {
+ *self.initialized.get_mut()
+ }
+
+ pub fn get(&self) -> Option<&T> {
+ if self.is_initialized() {
+ // SAFETY: The acquire load above observed publication of the value.
+ Some(unsafe { self.get_unchecked() })
+ } else {
+ None
+ }
+ }
+
+ pub fn get_mut(&mut self) -> Option<&mut T> {
+ if self.is_initialized_mut() {
+ // SAFETY: Exclusive access rules out concurrent reads or writes.
+ Some(unsafe { self.get_unchecked_mut() })
+ } else {
+ None
+ }
+ }
+
+ pub fn into_inner(mut self) -> Option<T> {
+ self.take()
+ }
+
+ pub fn take(&mut self) -> Option<T> {
+ if self.is_initialized_mut() {
+ *self.initialized.get_mut() = false;
+ // SAFETY: The value was initialized and the flag was cleared so it will not be
+ // dropped a second time.
+ Some(unsafe { self.value.get_mut().assume_init_read() })
+ } else {
+ None
+ }
+ }
+
+ /// Publishes a value after the containing primitive has won initialization.
+ ///
+ /// # Safety
+ ///
+ /// The cell must be uninitialized, and no other initialization may read or write it.
+ pub unsafe fn set(&self, value: T) -> &T {
+ debug_assert!(!self.is_initialized());
+ let value_ptr = self.value.get();
+ unsafe { value_ptr.write(MaybeUninit::new(value)) };
+
+ // Publish the initialized value to readers performing an acquire load.
+ self.initialized.store(true, Ordering::Release);
+
+ // SAFETY: The value was initialized and published above.
+ unsafe { self.get_unchecked() }
+ }
+
+ pub fn set_mut(&mut self, value: T) -> &mut T {
+ debug_assert!(!self.is_initialized_mut());
+ let value = self.value.get_mut().write(value);
+ *self.initialized.get_mut() = true;
+ value
+ }
+
+ /// # Safety
+ ///
+ /// The cell must be initialized.
+ unsafe fn get_unchecked(&self) -> &T {
+ debug_assert!(self.is_initialized());
+ unsafe { (&*self.value.get()).assume_init_ref() }
+ }
+
+ /// # Safety
+ ///
+ /// The cell must be initialized and exclusively borrowed.
+ unsafe fn get_unchecked_mut(&mut self) -> &mut T {
+ debug_assert!(self.is_initialized_mut());
+ unsafe { (&mut *self.value.get()).assume_init_mut() }
+ }
+}
+
+impl<T> Drop for ValueCell<T> {
+ fn drop(&mut self) {
+ if self.is_initialized_mut() {
+ // SAFETY: The value is initialized and exclusive access rules out other users.
+ unsafe { self.value.get_mut().assume_init_drop() };
+ }
+ }
+}
diff --git a/asyncband/src/lib.rs b/asyncband/src/lib.rs
index 1c3ffa5..41a52d8 100644
--- a/asyncband/src/lib.rs
+++ b/asyncband/src/lib.rs
@@ -54,7 +54,7 @@
//! | Use case | APIs | Cargo features |
//! | -------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------- |
//! | Protect shared state | [`mutex::Mutex`], [`rwlock::RwLock`], [`condvar::Condvar`] | `mutex`, `rwlock`, `condvar` |
-//! | Initialize values once | [`once::Once`], [`once::OnceCell`], [`once::OnceMap`] | `once`, `once-cell`, `once-map` |
+//! | Initialize values once | [`once::Once`], [`once::OnceCell`], [`once::LazyCell`], [`once::OnceMap`] | `once`, `once-cell`, `lazy-cell`, `once-map` |
//! | Coordinate tasks | [`barrier::Barrier`], [`latch::Latch`], [`waitgroup::WaitGroup`], [`shutdown`] | `barrier`, `latch`, `waitgroup`, `shutdown` |
//! | Send values | [`oneshot::channel`], [`mpsc::bounded`], [`mpsc::unbounded`] | `oneshot`, `mpsc` |
//! | Reuse objects | [`pool::bounded`], [`pool::unbounded`] | `pool` |
@@ -114,7 +114,12 @@ pub mod latch;
pub mod mpsc;
#[cfg(feature = "mutex")]
pub mod mutex;
-#[cfg(any(feature = "once", feature = "once-cell", feature = "once-map"))]
+#[cfg(any(
+ feature = "lazy-cell",
+ feature = "once",
+ feature = "once-cell",
+ feature = "once-map"
+))]
pub mod once;
#[cfg(feature = "oneshot")]
pub mod oneshot;
diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs
new file mode 100644
index 0000000..6d951da
--- /dev/null
+++ b/asyncband/src/once/lazy_cell/mod.rs
@@ -0,0 +1,273 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::fmt;
+use std::future::Future;
+use std::panic::RefUnwindSafe;
+use std::panic::UnwindSafe;
+use std::pin::Pin;
+use std::sync::atomic::AtomicBool;
+use std::sync::atomic::Ordering;
+
+use crate::internal::value_cell::ValueCell;
+use crate::mutex::Mutex;
+
+type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
+
+/// A thread-safe value initialized by a stored asynchronous function on first access.
+///
+/// Initialization starts when [`force`](Self::force) is polled. Concurrent callers wait without
+/// blocking their threads. If the forcing caller is cancelled, the initialization future remains
+/// pinned in the cell and the next caller resumes that same future instead of starting over.
+///
+/// The initialization future must be `Send + 'static`: another task may resume it after the
+/// original caller is gone, potentially on another thread. The initializer itself only needs to
+/// be `Send`, not `Sync`, because the cell serializes access to it.
+///
+/// `LazyCell` represents one asynchronous initialization attempt. If initialization needs
+/// access-time arguments or should retry after returning an error, use `OnceCell::get_or_try_init`
+/// instead. A `Result` may still be the stored value when an error should be cached.
+///
+/// # Poisoning
+///
+/// A panic while creating or polling the initialization future permanently poisons the cell. The
+/// panic is propagated to its caller, and future calls to [`force`](Self::force) or
+/// [`force_mut`](Self::force_mut) panic.
+///
+/// # Examples
+///
+/// ```
+/// # #[tokio::main]
+/// # async fn main() {
+/// use asyncband::once::LazyCell;
+///
+/// let lazy = LazyCell::<String, _>::new(async || "ready".to_owned());
+///
+/// assert_eq!(LazyCell::get(&lazy), None);
+/// assert_eq!(LazyCell::force(&lazy).await, "ready");
+/// assert_eq!(LazyCell::get(&lazy).map(String::as_str), Some("ready"));
+/// # }
+/// ```
+pub struct LazyCell<T, F = fn() -> BoxFuture<T>> {
+ value: ValueCell<T>,
+ state: Mutex<State<T, F>>,
+ poisoned: AtomicBool,
+}
+
+struct State<T, F> {
+ initializer: Option<F>,
+ attempt: Option<BoxFuture<T>>,
+}
+
+impl<T, F> State<T, F> {
+ async fn drive_attempt<Fut>(&mut self, poisoned: &AtomicBool) -> T
+ where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T> + Send + 'static,
+ {
+ if self.attempt.is_none() {
+ let initializer = self
+ .initializer
+ .take()
+ .expect("LazyCell initializer missing while uninitialized");
+ let future = {
+ let _poison = PoisonOnPanic(poisoned);
+ initializer()
+ };
+ self.attempt = Some(Box::pin(future));
+ }
+
+ let value = std::future::poll_fn(|cx| {
+ let _poison = PoisonOnPanic(poisoned);
+ self.attempt
+ .as_mut()
+ .expect("LazyCell attempt missing while initializing")
+ .as_mut()
+ .poll(cx)
+ })
+ .await;
+
+ // Treat panics from dropping a completed future as initializer panics as well.
+ let _poison = PoisonOnPanic(poisoned);
+ self.attempt = None;
+ value
+ }
+}
+
+impl<T, F> LazyCell<T, F> {
+ /// Creates a new lazy value with the given asynchronous initializer.
+ ///
+ /// The initializer is not called until the first [`force`](Self::force) future is polled.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # #[tokio::main]
+ /// # async fn main() {
+ /// use asyncband::once::LazyCell;
+ ///
+ /// let lazy = LazyCell::<u32, _>::new(async || 92);
+ /// assert_eq!(*LazyCell::force(&lazy).await, 92);
+ /// # }
+ /// ```
+ pub const fn new(initializer: F) -> Self {
+ Self {
+ value: ValueCell::new(),
+ state: Mutex::new(State {
+ initializer: Some(initializer),
+ attempt: None,
+ }),
+ poisoned: AtomicBool::new(false),
+ }
+ }
+
+ /// Returns a reference to the value if initialized.
+ ///
+ /// This method never starts initialization or waits for an active attempt. It returns `None`
+ /// when the cell is uninitialized, initializing, or poisoned.
+ pub fn get(this: &Self) -> Option<&T> {
+ this.value.get()
+ }
+
+ /// Returns a mutable reference to the value if initialized.
+ ///
+ /// This method never starts initialization. It returns `None` when the cell is uninitialized
+ /// or poisoned.
+ pub fn get_mut(this: &mut Self) -> Option<&mut T> {
+ this.value.get_mut()
+ }
+
+ /// Initializes the value if needed and returns a reference to it.
+ ///
+ /// If another task is initializing the cell, this call waits for that attempt. If the task
+ /// driving initialization is cancelled, a later caller resumes the same pinned future.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the initializer panics or the cell was previously poisoned. Recursive
+ /// initialization of the same cell deadlocks.
+ pub async fn force<Fut>(this: &Self) -> &T
+ where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T> + Send + 'static,
+ {
+ if let Some(value) = this.value.get() {
+ return value;
+ }
+ this.assert_unpoisoned();
+
+ let mut state = this.state.lock().await;
+ if let Some(value) = this.value.get() {
+ return value;
+ }
+ this.assert_unpoisoned();
+
+ let value = state.drive_attempt(&this.poisoned).await;
+
+ // SAFETY: The state mutex serializes initialization, and the double check above verified
+ // that the value was not initialized by another caller.
+ unsafe { this.value.set(value) }
+ }
+
+ /// Initializes the value if needed and returns mutable access to it.
+ ///
+ /// # Panics
+ ///
+ /// Panics under the same conditions as [`force`](Self::force).
+ pub async fn force_mut<Fut>(this: &mut Self) -> &mut T
+ where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T> + Send + 'static,
+ {
+ if this.value.is_initialized_mut() {
+ return this
+ .value
+ .get_mut()
+ .expect("LazyCell value missing while initialized");
+ }
+ if *this.poisoned.get_mut() {
+ panic_poisoned();
+ }
+
+ // Exclusive access makes locking and atomic value publication unnecessary.
+ let value = this.state.get_mut().drive_attempt(&this.poisoned).await;
+ this.value.set_mut(value)
+ }
+
+ fn assert_unpoisoned(&self) {
+ if self.poisoned.load(Ordering::Acquire) {
+ panic_poisoned();
+ }
+ }
+}
+
+impl<T> Default for LazyCell<T>
+where
+ T: Default,
+{
+ fn default() -> Self {
+ fn initialize<T: Default>() -> BoxFuture<T> {
+ Box::pin(async { T::default() })
+ }
+
+ Self::new(initialize::<T>)
+ }
+}
+
+impl<T, F> From<T> for LazyCell<T, F> {
+ fn from(value: T) -> Self {
+ Self {
+ value: ValueCell::from_value(value),
+ state: Mutex::new(State {
+ initializer: None,
+ attempt: None,
+ }),
+ poisoned: AtomicBool::new(false),
+ }
+ }
+}
+
+impl<T: fmt::Debug, F> fmt::Debug for LazyCell<T, F> {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut tuple = f.debug_tuple("LazyCell");
+ match Self::get(self) {
+ Some(value) => tuple.field(value),
+ None => tuple.field(&format_args!("<uninit>")),
+ };
+ tuple.finish()
+ }
+}
+
+impl<T: UnwindSafe, F: UnwindSafe> UnwindSafe for LazyCell<T, F> {}
+
+impl<T: RefUnwindSafe + UnwindSafe, F: UnwindSafe> RefUnwindSafe for LazyCell<T, F> {}
+
+struct PoisonOnPanic<'a>(&'a AtomicBool);
+
+impl Drop for PoisonOnPanic<'_> {
+ fn drop(&mut self) {
+ if std::thread::panicking() {
+ self.0.store(true, Ordering::Release);
+ }
+ }
+}
+
+#[cold]
+#[inline(never)]
+fn panic_poisoned() -> ! {
+ panic!("LazyCell instance has previously been poisoned")
+}
diff --git a/asyncband/src/once/mod.rs b/asyncband/src/once/mod.rs
index 82c5a65..70a43d2 100644
--- a/asyncband/src/once/mod.rs
+++ b/asyncband/src/once/mod.rs
@@ -17,6 +17,8 @@
//! Asynchronous primitives for one-time coordination.
+#[cfg(feature = "lazy-cell")]
+mod lazy_cell;
#[cfg(feature = "once")]
mod once;
#[cfg(feature = "once-cell")]
@@ -24,6 +26,8 @@ mod once_cell;
#[cfg(feature = "once-map")]
mod once_map;
+#[cfg(feature = "lazy-cell")]
+pub use self::lazy_cell::LazyCell;
#[cfg(feature = "once")]
pub use self::once::Once;
#[cfg(feature = "once-cell")]
diff --git a/asyncband/src/once/once_cell/mod.rs b/asyncband/src/once/once_cell/mod.rs
index 3fdd906..ddb2323 100644
--- a/asyncband/src/once/once_cell/mod.rs
+++ b/asyncband/src/once/once_cell/mod.rs
@@ -15,18 +15,20 @@
// specific language governing permissions and limitations
// under the License.
-use std::cell::UnsafeCell;
use std::convert::Infallible;
use std::fmt;
-use std::mem::MaybeUninit;
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering;
+use crate::internal::value_cell::ValueCell;
use crate::semaphore::Semaphore;
use crate::semaphore::SemaphorePermit;
/// A thread-safe cell which can nominally be written to only once.
///
+/// Callers provide an initializer when accessing an empty cell. An initializer that returns an
+/// error, panics, or is cancelled leaves the cell empty so a later caller can retry. Use
+/// `LazyCell` instead when the cell should own a one-shot initializer and preserve its in-flight
+/// future across caller cancellation.
+///
/// # Examples
///
/// ```
@@ -49,17 +51,10 @@ use crate::semaphore::SemaphorePermit;
/// The outputs must be either `Results: 1, 1` or `Results: 2, 2`, i.e. once the value is set via
/// an asynchronous function, the value inside the `OnceCell` will be immutable.
pub struct OnceCell<T> {
- value_set: AtomicBool,
- value: UnsafeCell<MaybeUninit<T>>,
+ value: ValueCell<T>,
semaphore: Semaphore,
}
-// SAFETY: OnceCell<T> can be shared between threads as long as T is Sync + Send.
-unsafe impl<T: Sync + Send> Sync for OnceCell<T> {}
-
-// SAFETY: OnceCell<T> can be sent between threads as long as T is Send.
-unsafe impl<T: Send> Send for OnceCell<T> {}
-
impl<T> Default for OnceCell<T> {
fn default() -> Self {
Self::new()
@@ -70,8 +65,7 @@ impl<T> OnceCell<T> {
/// Creates a new empty `OnceCell`.
pub const fn new() -> Self {
Self {
- value_set: AtomicBool::new(false),
- value: UnsafeCell::new(MaybeUninit::uninit()),
+ value: ValueCell::new(),
semaphore: Semaphore::new(1),
}
}
@@ -79,20 +73,22 @@ impl<T> OnceCell<T> {
/// Creates a new `OnceCell` initialized with the provided value.
pub const fn from_value(value: T) -> Self {
Self {
- value_set: AtomicBool::new(true),
- value: UnsafeCell::new(MaybeUninit::new(value)),
+ value: ValueCell::from_value(value),
semaphore: Semaphore::new(1),
}
}
/// Returns whether the internal value is set.
+ // `OnceMap` and `singleflight` inspect this state, while a standalone `OnceCell` build does
+ // not need the internal helper.
+ #[allow(dead_code)]
pub(crate) fn initialized(&self) -> bool {
- self.value_set.load(Ordering::Acquire)
+ self.value.is_initialized()
}
/// Returns whether the internal value is set.
pub(crate) fn initialized_mut(&mut self) -> bool {
- *self.value_set.get_mut()
+ self.value.is_initialized_mut()
}
/// Gets the reference to the underlying value.
@@ -101,11 +97,7 @@ impl<T> OnceCell<T> {
///
/// This method never blocks.
pub fn get(&self) -> Option<&T> {
- if self.initialized() {
- Some(unsafe { self.get_unchecked() })
- } else {
- None
- }
+ self.value.get()
}
/// Gets the mutable reference to the underlying value.
@@ -115,11 +107,7 @@ impl<T> OnceCell<T> {
/// This method never blocks. Since it borrows the `OnceCell` mutably, it is statically
/// guaranteed that no active borrows to the `OnceCell` exist, including from other threads.
pub fn get_mut(&mut self) -> Option<&mut T> {
- if self.initialized_mut() {
- Some(unsafe { self.get_unchecked_mut() })
- } else {
- None
- }
+ self.value.get_mut()
}
/// Gets the reference to the internal value, initializing it with the provided asynchronous
@@ -252,7 +240,10 @@ impl<T> OnceCell<T> {
// Workaround if let Some(v) = self.get_mut() { return Ok(v); }
// @see https://github.com/rust-lang/rust/issues/51545
if self.initialized_mut() {
- return Ok(unsafe { self.get_unchecked_mut() });
+ return Ok(self
+ .value
+ .get_mut()
+ .expect("OnceCell initialized value missing"));
}
let value = init().await?;
@@ -353,14 +344,8 @@ impl<T> OnceCell<T> {
/// assert_eq!(cell.into_inner(), Some("hello".to_string()));
/// # }
/// ```
- pub fn into_inner(mut self) -> Option<T> {
- if self.initialized_mut() {
- // set to uninitialized for the destructor of `OnceCell` to work properly
- *self.value_set.get_mut() = false;
- Some(unsafe { self.value.get_mut().assume_init_read() })
- } else {
- None
- }
+ pub fn into_inner(self) -> Option<T> {
+ self.value.into_inner()
}
/// Takes the value out of this `OnceCell`, moving it back to an uninitialized state.
@@ -387,55 +372,17 @@ impl<T> OnceCell<T> {
/// # }
/// ```
pub fn take(&mut self) -> Option<T> {
- std::mem::take(self).into_inner()
- }
-
- /// # Safety
- ///
- /// The cell must be initialized
- #[inline]
- unsafe fn get_unchecked(&self) -> &T {
- debug_assert!(self.initialized());
- unsafe { (&*self.value.get()).assume_init_ref() }
- }
-
- /// # Safety
- ///
- /// The cell must be initialized
- #[inline]
- unsafe fn get_unchecked_mut(&mut self) -> &mut T {
- debug_assert!(self.initialized_mut());
- unsafe { (&mut *self.value.get()).assume_init_mut() }
+ self.value.take()
}
fn set_value(&self, value: T, permit: SemaphorePermit<'_>) -> &T {
- // Hold the permit to ensure exclusive access.
let _permit = permit;
-
- let value_ptr = self.value.get();
- unsafe { value_ptr.write(MaybeUninit::new(value)) };
-
- // Use `store` with `Release` ordering to ensure that when loading it with `Acquire`
- // ordering, the initialized value is visible.
- self.value_set.store(true, Ordering::Release);
-
- // SAFETY: value initialized above
- unsafe { self.get_unchecked() }
+ // SAFETY: Holding the only semaphore permit serializes initialization.
+ unsafe { self.value.set(value) }
}
fn set_value_mut(&mut self, value: T) -> &mut T {
- let value = self.value.get_mut().write(value);
- *self.value_set.get_mut() = true;
- value
- }
-}
-
-impl<T> Drop for OnceCell<T> {
- fn drop(&mut self) {
- if self.initialized_mut() {
- // SAFETY: The cell is initialized and being dropped, so it can't be accessed again.
- unsafe { self.value.get_mut().assume_init_drop() };
- }
+ self.value.set_mut(value)
}
}
diff --git a/tests-integration/Cargo.toml b/examples/Cargo.toml
similarity index 64%
copy from tests-integration/Cargo.toml
copy to examples/Cargo.toml
index 27c0594..c0bf64f 100644
--- a/tests-integration/Cargo.toml
+++ b/examples/Cargo.toml
@@ -16,42 +16,22 @@
# under the License.
[package]
-name = "tests-integration"
+name = "examples"
publish = false
edition.workspace = true
rust-version.workspace = true
-[dependencies]
-tokio = { workspace = true, features = ["full"] }
-
-[dev-dependencies]
-asyncband = { workspace = true, features = [
- "barrier",
- "blocking",
- "condvar",
- "latch",
- "mpsc",
- "mutex",
- "once",
- "once-cell",
- "once-map",
- "oneshot",
- "pool",
- "rwlock",
- "semaphore",
- "shutdown",
- "singleflight",
- "waitgroup",
-] }
-pollster = { workspace = true, features = ["macro"] }
-tokio-test = { workspace = true }
+[package.metadata.release]
+release = false
-[lib]
-bench = false
-doc = false
-doctest = false
-test = false
+[dependencies]
+asyncband = { workspace = true, features = ["lazy-cell", "once-cell"] }
+tokio = { workspace = true, features = ["macros", "rt", "sync"] }
[lints]
workspace = true
+
+[[example]]
+name = "once_cell_vs_lazy_cell"
+path = "src/once_cell_vs_lazy_cell.rs"
diff --git a/examples/src/once_cell_vs_lazy_cell.rs b/examples/src/once_cell_vs_lazy_cell.rs
new file mode 100644
index 0000000..c546fdc
--- /dev/null
+++ b/examples/src/once_cell_vs_lazy_cell.rs
@@ -0,0 +1,129 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::future::Ready;
+use std::sync::Arc;
+use std::sync::atomic::AtomicUsize;
+use std::sync::atomic::Ordering;
+
+use asyncband::once::LazyCell;
+use asyncband::once::OnceCell;
+use tokio::sync::Notify;
+
+static ONCE_ENDPOINT: OnceCell<String> = OnceCell::new();
+static LAZY_ENDPOINT: LazyCell<String, fn() -> Ready<String>> =
+ LazyCell::new(load_default_endpoint);
+
+fn load_default_endpoint() -> Ready<String> {
+ std::future::ready("https://service.example".to_owned())
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
+ fixed_static_initializer_can_use_either_cell().await;
+ once_cell_accepts_access_time_context().await;
+ lazy_cell_owns_a_local_fn_once().await;
+}
+
+async fn fixed_static_initializer_can_use_either_cell() {
+ // When restarting after cancellation is acceptable, a stateless, repeatable initializer does
+ // not require `LazyCell`. An accessor can pass the same free function to `OnceCell`.
+ assert_eq!(
+ ONCE_ENDPOINT.get_or_init(load_default_endpoint).await,
+ "https://service.example"
+ );
+
+ // `LazyCell` instead stores that fixed initializer in the static itself, so callers only need
+ // to force the value.
+ assert_eq!(
+ LazyCell::force(&LAZY_ENDPOINT).await,
+ "https://service.example"
+ );
+}
+
+async fn once_cell_accepts_access_time_context() {
+ let endpoint = OnceCell::new();
+
+ // Failed attempts leave `OnceCell` empty. A later caller can retry with fresh context.
+ let first = endpoint
+ .get_or_try_init(async || Err::<String, _>("service discovery unavailable"))
+ .await;
+ assert_eq!(first.unwrap_err(), "service discovery unavailable");
+
+ let discovered_endpoint = "https://discovered.example".to_owned();
+ let endpoint = endpoint
+ .get_or_try_init(async move || Ok::<_, &'static str>(discovered_endpoint))
+ .await
+ .unwrap();
+ assert_eq!(endpoint, "https://discovered.example");
+}
+
+async fn lazy_cell_owns_a_local_fn_once() {
+ struct Credentials {
+ token: String,
+ }
+
+ struct Client {
+ token: String,
+ }
+
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let started = Arc::new(Notify::new());
+ let resume = Arc::new(Notify::new());
+ let credentials = Credentials {
+ token: "secret".to_owned(),
+ };
+
+ let initialize = {
+ let attempts = Arc::clone(&attempts);
+ let started = Arc::clone(&started);
+ let resume = Arc::clone(&resume);
+
+ // Moving `credentials.token` out makes this local initializer `FnOnce`, not `Fn`.
+ move || {
+ let token = credentials.token;
+
+ async move {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ started.notify_one();
+ resume.notified().await;
+ Client { token }
+ }
+ }
+ };
+
+ // A caller could pass `initialize` to `OnceCell::get_or_init` once, but the call consumes it.
+ // If that call is cancelled, a later caller cannot supply the same `FnOnce` again. `LazyCell`
+ // owns the initializer and preserves its in-flight future across callers.
+ let client = Arc::new(LazyCell::<Client, _>::new(initialize));
+
+ let first_caller = tokio::spawn({
+ let client = Arc::clone(&client);
+ async move {
+ LazyCell::force(&client).await;
+ }
+ });
+ started.notified().await;
+ first_caller.abort();
+ assert!(first_caller.await.unwrap_err().is_cancelled());
+
+ // Cancellation does not consume the captured credentials or restart the initializer. The next
+ // caller resumes the same future.
+ resume.notify_one();
+ assert_eq!(LazyCell::force(&client).await.token, "secret");
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
diff --git a/tests-integration/Cargo.toml b/tests-integration/Cargo.toml
index 27c0594..7ea541f 100644
--- a/tests-integration/Cargo.toml
+++ b/tests-integration/Cargo.toml
@@ -31,6 +31,7 @@ asyncband = { workspace = true, features = [
"blocking",
"condvar",
"latch",
+ "lazy-cell",
"mpsc",
"mutex",
"once",
diff --git a/tests-integration/tests/lazy_cell_test.rs b/tests-integration/tests/lazy_cell_test.rs
new file mode 100644
index 0000000..453b103
--- /dev/null
+++ b/tests-integration/tests/lazy_cell_test.rs
@@ -0,0 +1,309 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::cell::Cell;
+use std::future::Ready;
+use std::rc::Rc;
+use std::sync::Arc;
+use std::sync::atomic::AtomicUsize;
+use std::sync::atomic::Ordering;
+
+use asyncband::once::LazyCell;
+use tokio::sync::Notify;
+
+#[tokio::test]
+async fn initializer_starts_when_force_is_polled() {
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let lazy = LazyCell::<u32, _>::new({
+ let attempts = attempts.clone();
+ move || {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ async { 42 }
+ }
+ });
+
+ let force = LazyCell::force(&lazy);
+ assert_eq!(attempts.load(Ordering::SeqCst), 0);
+ drop(force);
+ assert_eq!(attempts.load(Ordering::SeqCst), 0);
+
+ assert_eq!(LazyCell::force(&lazy).await, &42);
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
+
+#[tokio::test]
+async fn concurrent_force_runs_initializer_once() {
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let attempts = attempts.clone();
+ async move || {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ tokio::task::yield_now().await;
+ 42
+ }
+ }));
+
+ let mut tasks = Vec::new();
+ for _ in 0..32 {
+ let lazy = lazy.clone();
+ tasks.push(tokio::spawn(async move { *LazyCell::force(&lazy).await }));
+ }
+
+ for task in tasks {
+ assert_eq!(task.await.unwrap(), 42);
+ }
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
+
+#[tokio::test]
+async fn cancellation_preserves_initialization_future() {
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let started = Arc::new(Notify::new());
+ let resume = Arc::new(Notify::new());
+ let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let attempts = attempts.clone();
+ let started = started.clone();
+ let resume = resume.clone();
+ async move || {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ started.notify_one();
+ resume.notified().await;
+ 42
+ }
+ }));
+
+ let task = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move { *LazyCell::force(&lazy).await })
+ };
+ started.notified().await;
+ assert_eq!(LazyCell::get(&lazy), None);
+
+ task.abort();
+ assert!(task.await.unwrap_err().is_cancelled());
+
+ resume.notify_one();
+ assert_eq!(*LazyCell::force(&lazy).await, 42);
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
+
+#[tokio::test]
+async fn unrelated_unwind_does_not_poison_pending_attempt() {
+ let started = Arc::new(Notify::new());
+ let resume = Arc::new(Notify::new());
+ let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let started = started.clone();
+ let resume = resume.clone();
+ async move || {
+ started.notify_one();
+ resume.notified().await;
+ 42
+ }
+ }));
+
+ let task = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move {
+ tokio::select! {
+ biased;
+ _ = LazyCell::force(&lazy) => {}
+ _ = async {
+ started.notified().await;
+ panic!("unrelated panic");
+ } => {}
+ }
+ })
+ };
+ assert!(task.await.unwrap_err().is_panic());
+
+ resume.notify_one();
+ assert_eq!(LazyCell::force(&lazy).await, &42);
+}
+
+#[tokio::test]
+async fn dropping_cell_drops_suspended_attempt() {
+ let held = Arc::new(());
+ let weak = Arc::downgrade(&held);
+ let started = Arc::new(Notify::new());
+ let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let started = started.clone();
+ async move || {
+ started.notify_one();
+ std::future::pending::<()>().await;
+ drop(held);
+ 42
+ }
+ }));
+
+ let task = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move { *LazyCell::force(&lazy).await })
+ };
+ started.notified().await;
+ task.abort();
+ assert!(task.await.unwrap_err().is_cancelled());
+
+ drop(Arc::try_unwrap(lazy).ok().unwrap());
+ assert!(weak.upgrade().is_none());
+}
+
+#[tokio::test]
+async fn result_is_cached_as_the_value() {
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let lazy = LazyCell::<Result<u32, &'static str>, _>::new({
+ let attempts = attempts.clone();
+ async move || {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ Err("cached")
+ }
+ });
+
+ assert_eq!(LazyCell::force(&lazy).await, &Err("cached"));
+ assert_eq!(LazyCell::force(&lazy).await, &Err("cached"));
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
+
+#[tokio::test]
+async fn initializer_poll_panic_permanently_poisons_cell() {
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let attempts = attempts.clone();
+ async move || {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ panic!("initializer panic");
+ }
+ }));
+
+ let first = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move {
+ let _ = LazyCell::force(&lazy).await;
+ })
+ };
+ assert!(first.await.unwrap_err().is_panic());
+ assert_eq!(LazyCell::get(&lazy), None);
+
+ let second = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move {
+ let _ = LazyCell::force(&lazy).await;
+ })
+ };
+ assert!(second.await.unwrap_err().is_panic());
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+
+ let mut lazy = Arc::try_unwrap(lazy).ok().unwrap();
+ let third = tokio::spawn(async move {
+ let _ = LazyCell::force_mut(&mut lazy).await;
+ });
+ assert!(third.await.unwrap_err().is_panic());
+}
+
+#[tokio::test]
+async fn initializer_creation_panic_permanently_poisons_cell() {
+ let lazy = Arc::new(LazyCell::<u32, _>::new(|| -> std::future::Ready<u32> {
+ panic!("initializer creation panic")
+ }));
+
+ let first = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move {
+ let _ = LazyCell::force(&lazy).await;
+ })
+ };
+ assert!(first.await.unwrap_err().is_panic());
+
+ let second = tokio::spawn(async move {
+ let _ = LazyCell::force(&lazy).await;
+ });
+ assert!(second.await.unwrap_err().is_panic());
+}
+
+#[tokio::test]
+async fn force_mut_updates_value() {
+ let mut lazy = LazyCell::<u32, _>::new(async || 41);
+ *LazyCell::force_mut(&mut lazy).await += 1;
+ assert_eq!(LazyCell::get(&lazy), Some(&42));
+}
+
+#[tokio::test]
+async fn force_mut_resumes_a_started_attempt() {
+ let started = Arc::new(Notify::new());
+ let resume = Arc::new(Notify::new());
+ let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let started = started.clone();
+ let resume = resume.clone();
+ async move || {
+ started.notify_one();
+ resume.notified().await;
+ 42
+ }
+ }));
+
+ let task = {
+ let lazy = lazy.clone();
+ tokio::spawn(async move {
+ let _ = LazyCell::force(&lazy).await;
+ })
+ };
+ started.notified().await;
+ task.abort();
+ assert!(task.await.unwrap_err().is_cancelled());
+
+ let mut lazy = Arc::try_unwrap(lazy).ok().unwrap();
+ resume.notify_one();
+ assert_eq!(LazyCell::force_mut(&mut lazy).await, &mut 42);
+}
+
+#[tokio::test]
+async fn default_from_and_debug_match_lazy_cell() {
+ let lazy = LazyCell::<u32>::default();
+ assert_eq!(format!("{lazy:?}"), "LazyCell(<uninit>)");
+ assert_eq!(LazyCell::force(&lazy).await, &0);
+ assert_eq!(format!("{lazy:?}"), "LazyCell(0)");
+
+ let local = LazyCell::<Rc<u32>>::default();
+ assert_eq!(**LazyCell::force(&local).await, 0);
+
+ let lazy: LazyCell<u32> = LazyCell::from(42);
+ assert_eq!(LazyCell::get(&lazy), Some(&42));
+}
+
+#[tokio::test]
+async fn initializer_need_not_be_sync() {
+ fn assert_sync<T: Sync>(_: &T) {}
+
+ let count = Cell::new(0);
+ let lazy = LazyCell::<u32, _>::new(async move || {
+ count.set(count.get() + 1);
+ count.get()
+ });
+
+ assert_sync(&lazy);
+ assert_eq!(LazyCell::force(&lazy).await, &1);
+}
+
+fn static_initializer() -> Ready<u32> {
+ std::future::ready(42)
+}
+
+static STATIC_LAZY: LazyCell<u32, fn() -> Ready<u32>> = LazyCell::new(static_initializer);
+
+#[tokio::test]
+async fn function_pointer_initializer_supports_statics() {
+ assert_eq!(LazyCell::force(&STATIC_LAZY).await, &42);
+}
diff --git a/tests-integration/tests/traits_test.rs b/tests-integration/tests/traits_test.rs
index 4419543..7872e29 100644
--- a/tests-integration/tests/traits_test.rs
+++ b/tests-integration/tests/traits_test.rs
@@ -23,6 +23,7 @@ use asyncband::latch::Latch;
use asyncband::mpsc;
use asyncband::mutex::Mutex;
use asyncband::mutex::MutexGuard;
+use asyncband::once::LazyCell;
use asyncband::once::Once;
use asyncband::once::OnceCell;
use asyncband::once::OnceMap;
@@ -67,6 +68,7 @@ fn public_types_are_send_and_sync() {
assert_send_and_sync::<Barrier>();
assert_send_and_sync::<Condvar>();
+ assert_send_and_sync::<LazyCell<u32>>();
assert_send_and_sync::<Once>();
assert_send_and_sync::<OnceCell<u32>>();
assert_send_and_sync::<OnceMap<String, u32>>();
@@ -114,6 +116,7 @@ fn public_types_are_unpin() {
assert_unpin::<Barrier>();
assert_unpin::<Condvar>();
assert_unpin::<Latch>();
+ assert_unpin::<LazyCell<u32>>();
assert_unpin::<Once>();
assert_unpin::<OnceCell<u32>>();
assert_unpin::<OnceMap<String, u32>>();
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@asyncband.apache.org
For additional commands, e-mail: commits-help@asyncband.apache.org