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 06:44:23 UTC
(asyncband) 01/01: refactor(lazy-cell): make future storage explicit
This is an automated email from the ASF dual-hosted git repository.
tisonkun pushed a commit to branch codex/lazy-cell-inline-future
in repository https://gitbox.apache.org/repos/asf/asyncband.git
commit 019b4bbaec08f801ea9ecaa0f7f27250f68271fa
Author: tison <wa...@gmail.com>
AuthorDate: Wed Aug 26 14:41:38 2026 +0800
refactor(lazy-cell): make future storage explicit
---
asyncband/src/once/lazy_cell/mod.rs | 284 +++++++++++++++++++-----------
examples/Cargo.toml | 4 +
examples/src/lazy_cell_boxed_vs_inline.rs | 61 +++++++
examples/src/once_cell_vs_lazy_cell.rs | 11 +-
tests-integration/tests/lazy_cell_test.rs | 163 +++++++++++------
tests-integration/tests/traits_test.rs | 4 +-
6 files changed, 366 insertions(+), 161 deletions(-)
diff --git a/asyncband/src/once/lazy_cell/mod.rs b/asyncband/src/once/lazy_cell/mod.rs
index a815555..efe134a 100644
--- a/asyncband/src/once/lazy_cell/mod.rs
+++ b/asyncband/src/once/lazy_cell/mod.rs
@@ -17,6 +17,8 @@
use std::fmt;
use std::future::Future;
+use std::future::Pending;
+use std::future::Ready;
use std::panic::RefUnwindSafe;
use std::panic::UnwindSafe;
use std::pin::Pin;
@@ -26,17 +28,20 @@ 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.
+/// `LazyCell` stores the initializer's concrete future type without allocating or erasing it. The
+/// caller chooses whether that future lives inline in the cell or behind a pointer such as
+/// `Pin<Box<_>>`.
+///
+/// An `Unpin` future can be initialized with [`force`](Self::force). Otherwise the cell itself must
+/// be pinned and initialized with [`force_pin`](Self::force_pin). If the forcing caller is
+/// cancelled, the initialization future remains 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.
+/// The cell's `Send` and `Sync` implementations follow its value, initializer, and future. A local
+/// future may therefore borrow local data or be non-`Send`, while a cell shared between threads
+/// naturally requires those stored types to be `Send`.
///
/// `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`
@@ -45,8 +50,7 @@ type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
/// # 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.
+/// panic is propagated to its caller, and future calls to any forcing method panic.
///
/// # Examples
///
@@ -55,63 +59,30 @@ type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
/// # async fn main() {
/// use asyncband::once::LazyCell;
///
-/// let lazy = LazyCell::<String, _>::new(async || "ready".to_owned());
+/// let lazy = LazyCell::new(async || "ready".to_owned());
+/// let lazy = std::pin::pin!(lazy);
///
/// assert_eq!(LazyCell::get(&lazy), None);
-/// assert_eq!(LazyCell::force(&lazy).await, "ready");
+/// assert_eq!(LazyCell::force_pin(lazy.as_ref()).await, "ready");
/// assert_eq!(LazyCell::get(&lazy).map(String::as_str), Some("ready"));
/// # }
/// ```
-pub struct LazyCell<T, F = fn() -> BoxFuture<T>> {
+pub struct LazyCell<T, Fut, F = fn() -> Fut> {
value: ValueCell<T>,
- state: Mutex<State<T, F>>,
+ state: Mutex<State<F, Fut>>,
poisoned: AtomicBool,
}
-struct State<T, F> {
+struct State<F, Fut> {
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
- }
+ attempt: Option<Fut>,
}
-impl<T, F> LazyCell<T, F> {
+impl<T, Fut, F> LazyCell<T, Fut, 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.
+ /// The initializer is not called until the first forcing future is polled. Its returned future
+ /// is stored as-is; this method never boxes it.
///
/// # Examples
///
@@ -120,11 +91,28 @@ impl<T, F> LazyCell<T, F> {
/// # async fn main() {
/// use asyncband::once::LazyCell;
///
- /// let lazy = LazyCell::<u32, _>::new(async || 92);
+ /// let lazy = LazyCell::new(async || 92);
+ /// let lazy = std::pin::pin!(lazy);
+ /// assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 92);
+ /// # }
+ /// ```
+ ///
+ /// Or explicitly box the future so the cell remains movable:
+ ///
+ /// ```
+ /// # #[tokio::main]
+ /// # async fn main() {
+ /// use asyncband::once::LazyCell;
+ ///
+ /// let lazy = LazyCell::new(|| Box::pin(async { 92 }));
/// assert_eq!(*LazyCell::force(&lazy).await, 92);
/// # }
/// ```
- pub const fn new(initializer: F) -> Self {
+ pub const fn new(initializer: F) -> Self
+ where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T>,
+ {
Self {
value: ValueCell::new(),
state: Mutex::new(State {
@@ -151,48 +139,90 @@ impl<T, F> LazyCell<T, F> {
this.value.get_mut()
}
+ fn assert_unpoisoned(&self) {
+ if self.poisoned.load(Ordering::Acquire) {
+ panic_poisoned();
+ }
+ }
+}
+
+impl<T, Fut, F> LazyCell<T, Fut, F>
+where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T> + Unpin,
+{
/// Initializes the value if needed and returns a reference to it.
///
+ /// This method is available when moving the stored future is safe. A future behind
+ /// `Pin<Box<_>>` is `Unpin` because moving the box does not move its pointee.
+ ///
/// 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.
+ /// driving initialization is cancelled, a later caller resumes the same 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() {
+ pub async fn force(this: &Self) -> &T {
+ Self::force_pin(Pin::new(this)).await
+ }
+
+ /// 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(this: &mut Self) -> &mut T {
+ Self::force_pin_mut(Pin::new(this)).await
+ }
+}
+
+impl<T, Fut, F> LazyCell<T, Fut, F>
+where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T>,
+{
+ /// Initializes a pinned cell and returns a reference to its value.
+ ///
+ /// Pinning the cell keeps an inline initialization future at a stable address. Cancellation
+ /// leaves that future in the cell so a later caller can resume it.
+ ///
+ /// # Panics
+ ///
+ /// Panics if the initializer panics or the cell was previously poisoned. Recursive
+ /// initialization of the same cell deadlocks.
+ pub async fn force_pin<'a>(this: Pin<&'a Self>) -> &'a T {
+ let this_ref: &'a Self = Pin::get_ref(this);
+ if let Some(value) = Self::get(this_ref) {
return value;
}
- this.assert_unpoisoned();
+ this_ref.assert_unpoisoned();
- let mut state = this.state.lock().await;
- if let Some(value) = this.value.get() {
+ let mut state = this_ref.state.lock().await;
+ if let Some(value) = Self::get(this_ref) {
return value;
}
- this.assert_unpoisoned();
+ this_ref.assert_unpoisoned();
- let value = state.drive_attempt(&this.poisoned).await;
+ // SAFETY: `this` remains pinned for the duration of the returned future. The state is
+ // stored in that cell, and a mutex guard never relocates its protected value.
+ let state = unsafe { Pin::new_unchecked(&mut *state) };
+ let value = drive_pinned_attempt(state, &this_ref.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) }
+ unsafe { this_ref.value.set(value) }
}
- /// Initializes the value if needed and returns mutable access to it.
+ /// Initializes a pinned cell and returns mutable access to its value.
///
/// # 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,
- {
+ /// Panics under the same conditions as [`force_pin`](Self::force_pin).
+ pub async fn force_pin_mut<'a>(this: Pin<&'a mut Self>) -> &'a mut T {
+ // SAFETY: We do not move the structurally pinned future through this reference. The stored
+ // value is not structurally pinned and may be accessed normally.
+ let this: &'a mut Self = unsafe { Pin::get_unchecked_mut(this) };
if this.value.is_initialized_mut() {
return this
.value
@@ -203,37 +233,91 @@ impl<T, F> LazyCell<T, F> {
panic_poisoned();
}
- // Exclusive access makes locking and atomic value publication unnecessary.
- let value = this.state.get_mut().drive_attempt(&this.poisoned).await;
+ // SAFETY: Exclusive access to the pinned cell pins its state in place for this call.
+ let state = unsafe { Pin::new_unchecked(this.state.get_mut()) };
+ let value = drive_pinned_attempt(state, &this.poisoned).await;
this.value.set_mut(value)
}
+}
- fn assert_unpoisoned(&self) {
- if self.poisoned.load(Ordering::Acquire) {
- panic_poisoned();
+async fn drive_pinned_attempt<T, F, Fut>(
+ mut state: Pin<&mut State<F, Fut>>,
+ poisoned: &AtomicBool,
+) -> T
+where
+ F: FnOnce() -> Fut,
+ Fut: Future<Output = T>,
+{
+ if state.as_ref().get_ref().attempt.is_none() {
+ // SAFETY: The initializer is not structurally pinned and the attempt is still empty. The
+ // newly created future is written directly into its final pinned location.
+ let state = unsafe { state.as_mut().get_unchecked_mut() };
+ let initializer = state
+ .initializer
+ .take()
+ .expect("LazyCell initializer missing while uninitialized");
+ let future = {
+ let _poison = PoisonOnPanic(poisoned);
+ initializer()
+ };
+ state.attempt = Some(future);
+ }
+
+ let value = std::future::poll_fn(|cx| {
+ let _poison = PoisonOnPanic(poisoned);
+ // SAFETY: The state remains pinned while this future is polled, and the attempt is never
+ // moved after it is installed.
+ let state = unsafe { state.as_mut().get_unchecked_mut() };
+ let attempt = state
+ .attempt
+ .as_mut()
+ .expect("LazyCell attempt missing while initializing");
+ unsafe { Pin::new_unchecked(attempt) }.poll(cx)
+ })
+ .await;
+
+ // Assignment drops the completed future in place before clearing the slot. Treat a panic from
+ // that destructor as an initializer panic as well.
+ let _poison = PoisonOnPanic(poisoned);
+ // SAFETY: Dropping a pinned value in place is permitted, and no value is moved out.
+ unsafe { state.as_mut().get_unchecked_mut() }.attempt = None;
+ value
+}
+
+impl<T> Default for LazyCell<T, Ready<T>>
+where
+ T: Default,
+{
+ fn default() -> Self {
+ fn initialize<T: Default>() -> Ready<T> {
+ std::future::ready(T::default())
}
+
+ Self::new(initialize::<T>)
}
}
-impl<T> LazyCell<T> {
+impl<T, Fut> LazyCell<T, Fut>
+where
+ Fut: Future<Output = T>,
+{
/// Creates a new `LazyCell` from an already-created asynchronous future.
///
- /// The future is stored without being polled. Calling [`force`](Self::force) starts polling
- /// it, and cancellation preserves the in-flight future for a later caller.
- pub fn from_future<Fut>(future: Fut) -> Self
- where
- Fut: Future<Output = T> + Send + 'static,
- {
+ /// The future is stored as-is without being polled, boxed, or erased. Pin the returned cell
+ /// before forcing it when `Fut` is not `Unpin`.
+ pub fn from_future(future: Fut) -> Self {
Self {
value: ValueCell::new(),
state: Mutex::new(State {
initializer: None,
- attempt: Some(Box::pin(future)),
+ attempt: Some(future),
}),
poisoned: AtomicBool::new(false),
}
}
+}
+impl<T> LazyCell<T, Pending<T>> {
/// Creates a new `LazyCell` that already contains `value`.
pub const fn from_value(value: T) -> Self {
Self {
@@ -247,20 +331,7 @@ impl<T> LazyCell<T> {
}
}
-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: fmt::Debug, F> fmt::Debug for LazyCell<T, F> {
+impl<T: fmt::Debug, Fut, F> fmt::Debug for LazyCell<T, Fut, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut tuple = f.debug_tuple("LazyCell");
match Self::get(self) {
@@ -271,9 +342,14 @@ impl<T: fmt::Debug, F> fmt::Debug for LazyCell<T, F> {
}
}
-impl<T: UnwindSafe, F: UnwindSafe> UnwindSafe for LazyCell<T, F> {}
+impl<T, Fut: Unpin, F> Unpin for LazyCell<T, Fut, F> {}
-impl<T: RefUnwindSafe + UnwindSafe, F: UnwindSafe> RefUnwindSafe for LazyCell<T, F> {}
+impl<T: UnwindSafe, Fut: UnwindSafe, F: UnwindSafe> UnwindSafe for LazyCell<T, Fut, F> {}
+
+impl<T: RefUnwindSafe + UnwindSafe, Fut: UnwindSafe, F: UnwindSafe> RefUnwindSafe
+ for LazyCell<T, Fut, F>
+{
+}
struct PoisonOnPanic<'a>(&'a AtomicBool);
diff --git a/examples/Cargo.toml b/examples/Cargo.toml
index c0bf64f..09b216f 100644
--- a/examples/Cargo.toml
+++ b/examples/Cargo.toml
@@ -35,3 +35,7 @@ workspace = true
[[example]]
name = "once_cell_vs_lazy_cell"
path = "src/once_cell_vs_lazy_cell.rs"
+
+[[example]]
+name = "lazy_cell_boxed_vs_inline"
+path = "src/lazy_cell_boxed_vs_inline.rs"
diff --git a/examples/src/lazy_cell_boxed_vs_inline.rs b/examples/src/lazy_cell_boxed_vs_inline.rs
new file mode 100644
index 0000000..8feb15e
--- /dev/null
+++ b/examples/src/lazy_cell_boxed_vs_inline.rs
@@ -0,0 +1,61 @@
+// 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::sync::Arc;
+
+use asyncband::once::LazyCell;
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
+ // The library stores exactly the future returned by the initializer. Explicitly boxing that
+ // future keeps the cell movable, without requiring dynamic dispatch.
+ let boxed = LazyCell::new(|| Box::pin(async { "boxed".to_owned() }));
+ assert_eq!(LazyCell::force(&boxed).await, "boxed");
+
+ // Returning the async future directly stores it inline. The caller pins the cell before the
+ // first poll, so the future stays at a stable address without a heap allocation.
+ let inline = LazyCell::new(async || "inline".to_owned());
+ let mut inline = std::pin::pin!(inline);
+ assert_eq!(LazyCell::force_pin(inline.as_ref()).await, "inline");
+
+ LazyCell::force_pin_mut(inline.as_mut())
+ .await
+ .push_str(" future");
+ assert_eq!(
+ LazyCell::get(&inline).expect("value is initialized"),
+ "inline future"
+ );
+
+ // Arc::pin allocates the cell once and keeps its inline future at a stable address. Cloning the
+ // pinned Arc lets multiple tasks resume the same future without a separate future allocation.
+ let shared = Arc::pin(LazyCell::new(async || {
+ tokio::task::yield_now().await;
+ "shared".to_owned()
+ }));
+
+ let first = tokio::spawn({
+ let shared = shared.clone();
+ async move { LazyCell::force_pin(shared.as_ref()).await.clone() }
+ });
+ let second = tokio::spawn({
+ let shared = shared.clone();
+ async move { LazyCell::force_pin(shared.as_ref()).await.clone() }
+ });
+
+ assert_eq!(first.await.unwrap(), "shared");
+ assert_eq!(second.await.unwrap(), "shared");
+}
diff --git a/examples/src/once_cell_vs_lazy_cell.rs b/examples/src/once_cell_vs_lazy_cell.rs
index c546fdc..726a117 100644
--- a/examples/src/once_cell_vs_lazy_cell.rs
+++ b/examples/src/once_cell_vs_lazy_cell.rs
@@ -25,8 +25,7 @@ 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);
+static LAZY_ENDPOINT: LazyCell<String, Ready<String>> = LazyCell::new(load_default_endpoint);
fn load_default_endpoint() -> Ready<String> {
std::future::ready("https://service.example".to_owned())
@@ -109,12 +108,12 @@ async fn lazy_cell_owns_a_local_fn_once() {
// 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 client = Arc::pin(LazyCell::new(initialize));
let first_caller = tokio::spawn({
- let client = Arc::clone(&client);
+ let client = client.clone();
async move {
- LazyCell::force(&client).await;
+ LazyCell::force_pin(client.as_ref()).await;
}
});
started.notified().await;
@@ -124,6 +123,6 @@ async fn lazy_cell_owns_a_local_fn_once() {
// 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!(LazyCell::force_pin(client.as_ref()).await.token, "secret");
assert_eq!(attempts.load(Ordering::SeqCst), 1);
}
diff --git a/tests-integration/tests/lazy_cell_test.rs b/tests-integration/tests/lazy_cell_test.rs
index 419e93e..b3bb9da 100644
--- a/tests-integration/tests/lazy_cell_test.rs
+++ b/tests-integration/tests/lazy_cell_test.rs
@@ -28,11 +28,11 @@ 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 lazy = LazyCell::new({
let attempts = attempts.clone();
move || {
attempts.fetch_add(1, Ordering::SeqCst);
- async { 42 }
+ Box::pin(async { 42 })
}
});
@@ -48,12 +48,14 @@ async fn initializer_starts_when_force_is_polled() {
#[tokio::test]
async fn concurrent_force_runs_initializer_once() {
let attempts = Arc::new(AtomicUsize::new(0));
- let lazy = Arc::new(LazyCell::<u32, _>::new({
+ let lazy = Arc::new(LazyCell::new({
let attempts = attempts.clone();
- async move || {
- attempts.fetch_add(1, Ordering::SeqCst);
- tokio::task::yield_now().await;
- 42
+ move || {
+ Box::pin(async move {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ tokio::task::yield_now().await;
+ 42
+ })
}
}));
@@ -74,15 +76,17 @@ 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 lazy = Arc::new(LazyCell::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
+ move || {
+ Box::pin(async move {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ started.notify_one();
+ resume.notified().await;
+ 42
+ })
}
}));
@@ -111,9 +115,10 @@ async fn from_future_does_not_poll_until_forced() {
42
}
});
+ let lazy = std::pin::pin!(lazy);
assert_eq!(polls.load(Ordering::SeqCst), 0);
- assert_eq!(LazyCell::force(&lazy).await, &42);
+ assert_eq!(LazyCell::force_pin(lazy.as_ref()).await, &42);
assert_eq!(polls.load(Ordering::SeqCst), 1);
}
@@ -122,7 +127,7 @@ async fn from_future_cancellation_preserves_the_same_future() {
let executions = Arc::new(AtomicUsize::new(0));
let started = Arc::new(Notify::new());
let resume = Arc::new(Notify::new());
- let lazy = Arc::new(LazyCell::from_future({
+ let lazy = Arc::pin(LazyCell::from_future({
let executions = executions.clone();
let started = started.clone();
let resume = resume.clone();
@@ -136,14 +141,14 @@ async fn from_future_cancellation_preserves_the_same_future() {
let task = {
let lazy = lazy.clone();
- tokio::spawn(async move { *LazyCell::force(&lazy).await })
+ tokio::spawn(async move { *LazyCell::force_pin(lazy.as_ref()).await })
};
started.notified().await;
task.abort();
assert!(task.await.unwrap_err().is_cancelled());
resume.notify_one();
- assert_eq!(*LazyCell::force(&lazy).await, 42);
+ assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 42);
assert_eq!(executions.load(Ordering::SeqCst), 1);
}
@@ -151,13 +156,15 @@ async fn from_future_cancellation_preserves_the_same_future() {
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 lazy = Arc::new(LazyCell::new({
let started = started.clone();
let resume = resume.clone();
- async move || {
- started.notify_one();
- resume.notified().await;
- 42
+ move || {
+ Box::pin(async move {
+ started.notify_one();
+ resume.notified().await;
+ 42
+ })
}
}));
@@ -185,13 +192,15 @@ 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 lazy = Arc::new(LazyCell::new({
let started = started.clone();
- async move || {
- started.notify_one();
- std::future::pending::<()>().await;
- drop(held);
- 42
+ move || {
+ Box::pin(async move {
+ started.notify_one();
+ std::future::pending::<()>().await;
+ drop(held);
+ 42
+ })
}
}));
@@ -210,11 +219,13 @@ async fn dropping_cell_drops_suspended_attempt() {
#[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 lazy = LazyCell::new({
let attempts = attempts.clone();
- async move || {
- attempts.fetch_add(1, Ordering::SeqCst);
- Err("cached")
+ move || {
+ Box::pin(async move {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ Err::<u32, _>("cached")
+ })
}
});
@@ -226,11 +237,13 @@ async fn result_is_cached_as_the_value() {
#[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 lazy = Arc::new(LazyCell::new({
let attempts = attempts.clone();
- async move || {
- attempts.fetch_add(1, Ordering::SeqCst);
- panic!("initializer panic");
+ move || {
+ Box::pin(async move {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ panic!("initializer panic");
+ })
}
}));
@@ -261,7 +274,7 @@ async fn initializer_poll_panic_permanently_poisons_cell() {
#[tokio::test]
async fn initializer_creation_panic_permanently_poisons_cell() {
- let lazy = Arc::new(LazyCell::<u32, _>::new(|| -> std::future::Ready<u32> {
+ let lazy = Arc::new(LazyCell::new(|| -> std::future::Ready<u32> {
panic!("initializer creation panic")
}));
@@ -281,7 +294,7 @@ async fn initializer_creation_panic_permanently_poisons_cell() {
#[tokio::test]
async fn force_mut_updates_value() {
- let mut lazy = LazyCell::<u32, _>::new(async || 41);
+ let mut lazy = LazyCell::new(|| std::future::ready(41));
*LazyCell::force_mut(&mut lazy).await += 1;
assert_eq!(LazyCell::get(&lazy), Some(&42));
}
@@ -290,13 +303,15 @@ async fn force_mut_updates_value() {
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 lazy = Arc::new(LazyCell::new({
let started = started.clone();
let resume = resume.clone();
- async move || {
- started.notify_one();
- resume.notified().await;
- 42
+ move || {
+ Box::pin(async move {
+ started.notify_one();
+ resume.notified().await;
+ 42
+ })
}
}));
@@ -317,12 +332,12 @@ async fn force_mut_resumes_a_started_attempt() {
#[tokio::test]
async fn default_and_value_constructors_match_lazy_cell() {
- let lazy = LazyCell::<u32>::default();
+ let lazy = LazyCell::<u32, Ready<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();
+ let local = LazyCell::<Rc<u32>, Ready<Rc<u32>>>::default();
assert_eq!(**LazyCell::force(&local).await, 0);
let lazy = const { LazyCell::from_value(42) };
@@ -334,9 +349,11 @@ 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()
+ let lazy = LazyCell::new(move || {
+ Box::pin(async move {
+ count.set(count.get() + 1);
+ count.get()
+ })
});
assert_sync(&lazy);
@@ -347,9 +364,57 @@ fn static_initializer() -> Ready<u32> {
std::future::ready(42)
}
-static STATIC_LAZY: LazyCell<u32, fn() -> Ready<u32>> = LazyCell::new(static_initializer);
+static STATIC_LAZY: LazyCell<u32, Ready<u32>> = LazyCell::new(static_initializer);
#[tokio::test]
async fn function_pointer_initializer_supports_statics() {
assert_eq!(LazyCell::force(&STATIC_LAZY).await, &42);
}
+
+#[tokio::test]
+async fn inline_future_supports_local_non_send_state() {
+ let local = Rc::new(41);
+ let lazy = LazyCell::new({
+ let local = Rc::clone(&local);
+ async move || {
+ tokio::task::yield_now().await;
+ *local + 1
+ }
+ });
+ let mut lazy = std::pin::pin!(lazy);
+
+ assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 42);
+ assert_eq!(LazyCell::get(&lazy), Some(&42));
+
+ *LazyCell::force_pin_mut(lazy.as_mut()).await += 1;
+ assert_eq!(LazyCell::get(&lazy), Some(&43));
+}
+
+#[tokio::test]
+async fn cancellation_preserves_inline_initialization_future() {
+ let attempts = Arc::new(AtomicUsize::new(0));
+ let started = Arc::new(Notify::new());
+ let resume = Arc::new(Notify::new());
+ let lazy = LazyCell::new({
+ let attempts = Arc::clone(&attempts);
+ let started = Arc::clone(&started);
+ let resume = Arc::clone(&resume);
+ async move || {
+ attempts.fetch_add(1, Ordering::SeqCst);
+ started.notify_one();
+ resume.notified().await;
+ 42
+ }
+ });
+ let lazy = std::pin::pin!(lazy);
+
+ tokio::select! {
+ biased;
+ _ = LazyCell::force_pin(lazy.as_ref()) => panic!("initializer unexpectedly completed"),
+ _ = started.notified() => {}
+ }
+
+ resume.notify_one();
+ assert_eq!(*LazyCell::force_pin(lazy.as_ref()).await, 42);
+ assert_eq!(attempts.load(Ordering::SeqCst), 1);
+}
diff --git a/tests-integration/tests/traits_test.rs b/tests-integration/tests/traits_test.rs
index 7872e29..012ae35 100644
--- a/tests-integration/tests/traits_test.rs
+++ b/tests-integration/tests/traits_test.rs
@@ -68,7 +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::<LazyCell<u32, std::future::Ready<u32>>>();
assert_send_and_sync::<Once>();
assert_send_and_sync::<OnceCell<u32>>();
assert_send_and_sync::<OnceMap<String, u32>>();
@@ -116,7 +116,7 @@ fn public_types_are_unpin() {
assert_unpin::<Barrier>();
assert_unpin::<Condvar>();
assert_unpin::<Latch>();
- assert_unpin::<LazyCell<u32>>();
+ assert_unpin::<LazyCell<u32, std::future::Ready<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