Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OnceCell #3591

Merged
merged 15 commits into from Apr 5, 2021
Merged

Add OnceCell #3591

merged 15 commits into from Apr 5, 2021

Conversation

b-naber
Copy link
Contributor

@b-naber b-naber commented Mar 8, 2021

Separating #3513 into two PRs for clarity, as requested by @Darksonn. Since Lazy depends on OnceCell, a PR for Lazy will be opened once this PR lands.

@Darksonn Darksonn added A-tokio Area: The main tokio crate M-sync Module: tokio/sync labels Mar 8, 2021
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Show resolved Hide resolved
tokio/src/sync/once_cell.rs Show resolved Hide resolved
tokio/src/sync/once_cell.rs Show resolved Hide resolved
@b-naber
Copy link
Contributor Author

b-naber commented Mar 12, 2021

@Darksonn Thanks for the review. I addressed your comments.

tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
tokio/src/sync/once_cell.rs Outdated Show resolved Hide resolved
@b-naber
Copy link
Contributor Author

b-naber commented Mar 17, 2021

@Darksonn Not sure about what to do about the clippy errors in the async_send_sync tests. I tried to use type aliases, but get 'never used' warnings for some reason.

Edit: In the previous commit I got errors in the clippy pass because the type for the function pointer was 'too complex' in the ?async_send_sync tests. The suggestion was to use type definitions, but apparently this is not the right way to handle this either.

@Darksonn
Copy link
Contributor

Feel free to silence "too complex" errors in the async_send_sync.rs file. You can add an #![allow(whatever_the_lint_name_is)] at the top to do so.

@b-naber b-naber force-pushed the once_cell branch 3 times, most recently from ed48b81 to b6c6d99 Compare March 21, 2021 21:43
@b-naber
Copy link
Contributor Author

b-naber commented Mar 21, 2021

@Darksonn CI passes now.

Comment on lines 240 to 252
/// Moves the value out of the cell and drops the cell afterwards.
pub fn into_inner(self) -> Result<T, NotInitializedError> {
if self.initialized() {
Ok(unsafe { self.value.with(|ptr| ptr::read(ptr).assume_init()) })
} else {
Err(NotInitializedError(()))
}
}

/// Takes ownership of the current value, leaving the cell unitialized.
pub fn take(&mut self) -> Result<T, NotInitializedError> {
if self.initialized() {
// Note: ptr::read does not move the value out of `self.value`. We need to use
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these also return Option for consistency with get?

Comment on lines 250 to 330
pub fn take(&mut self) -> Result<T, NotInitializedError> {
if self.initialized() {
// Note: ptr::read does not move the value out of `self.value`. We need to use
// `self.initialized` to prevent incorrect accesses to value
let value = unsafe { self.value.with(|ptr| ptr::read(ptr).assume_init()) };
*self.value_set.get_mut() = false;
Ok(value)
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still leaves the semaphore closed. Consider changing the implementation to:

let old_me = std::mem::replace(self, OnceCell::new());
old_me.into_inner()

Comment on lines 58 to 65
let new_cell = OnceCell::new();
if let Some(value) = self.get() {
match new_cell.set(value.clone()) {
Ok(()) => (),
Err(_) => unreachable!(),
}
}
new_cell
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kinda ugly. Should we have a constructor that takes an Option<T> and returns an already initialized cell if the option is some?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that's a good idea.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be a good name for that? new_with maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think new_with is an ok name.

@b-naber
Copy link
Contributor Author

b-naber commented Mar 22, 2021

@Darksonn addressed your comments.


impl<T: Clone> Clone for OnceCell<T> {
fn clone(&self) -> OnceCell<T> {
OnceCell::new_with(self.get().map(|v| (*v).clone()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use Option::cloned here.

Suggested change
OnceCell::new_with(self.get().map(|v| (*v).clone()))
OnceCell::new_with(self.get().cloned())

Comment on lines 85 to 107
let (value_set, value) = if let Some(v) = value {
(AtomicBool::new(true), UnsafeCell::new(MaybeUninit::new(v)))
} else {
(
AtomicBool::new(false),
UnsafeCell::new(MaybeUninit::uninit()),
)
};
OnceCell {
value_set,
value,
semaphore: Semaphore::new(1),
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to close the semaphore in the case where the value is set.

Suggested change
let (value_set, value) = if let Some(v) = value {
(AtomicBool::new(true), UnsafeCell::new(MaybeUninit::new(v)))
} else {
(
AtomicBool::new(false),
UnsafeCell::new(MaybeUninit::uninit()),
)
};
OnceCell {
value_set,
value,
semaphore: Semaphore::new(1),
}
if let Some(v) = value {
let semaphore = Semaphore::new(0);
semaphore.close();
OnceCell {
value_set: AtomicBool::new(true),
value: UnsafeCell::new(MaybeUninit::new(v)),
semaphore,
}
} else {
OnceCell::new()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that this is more consistent, but it should't matter in practice since we can only acquire the permit when value_set is true, which we set to false if value is Some.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do want the semaphore to be closed here, because it makes the safety arguments you have elsewhere easier to follow if the semaphore is always closed when the value is set.

Comment on lines 256 to 274
pub fn into_inner(self) -> Option<T> {
if self.initialized() {
Some(unsafe { self.value.with(|ptr| ptr::read(ptr).assume_init()) })
} else {
None
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a test that:

  1. The destructor of the T inside is called when the cell is dropped normally.
  2. The destructor of the T inside is not called when into_inner is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you check whether the destructor was called in rust?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess just implementing a new type including a drop implementation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can define a global counter that you increment in a custom destructor.

static NUM_DROPS: AtomicU32 = AtomicU32::new(0);

Make sure to use different counters for each test. They may run in parallel.

Comment on lines 268 to 273
if self.initialized() {
let old_me = std::mem::replace(self, OnceCell::new());
old_me.into_inner()
} else {
None
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified.

Suggested change
if self.initialized() {
let old_me = std::mem::replace(self, OnceCell::new());
old_me.into_inner()
} else {
None
}
let old_me = std::mem::replace(self, OnceCell::new());
old_me.into_inner()

Copy link
Sponsor Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mem::take too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that makes it even simpler.

Suggested change
if self.initialized() {
let old_me = std::mem::replace(self, OnceCell::new());
old_me.into_inner()
} else {
None
}
std::mem::take(self).into_inner()

@b-naber
Copy link
Contributor Author

b-naber commented Mar 24, 2021

@Darksonn So the new tests don't work for some reason. When OnceCell is dropped the drop method of its value is not called and into_inner does call drop. I don't understand why this happens.

edit: actually into_inner doesn't call drop. I didn't know that assigning to a _ is equivalent to dropping the assigned value.

edit2: MaybeUninit apparently never calls drop on its value, so we need a drop implementation for OnceCell, don't we?

Comment on lines 184 to 187
let _ = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning to underscore runs the destructor.

Suggested change
let _ = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
let fooer = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
drop(fooer);
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 1);

@Darksonn
Copy link
Contributor

We're going to need a destructor, yes.

@b-naber
Copy link
Contributor Author

b-naber commented Mar 24, 2021

@Darksonn can you take another look, please?

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any further major comments.

pub fn into_inner(self) -> Option<T> {
if self.initialized() {
// Set to uninitialized for the destructor of `OnceCell` to work properly
self.value_set.store(false, Ordering::Release);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whenever you are in self or &mut self methods, you can access it like this:

Suggested change
self.value_set.store(false, Ordering::Release);
*self.value_set.get_mut() = false;

Doing it in this manner is cheaper because it doesn't involve atomic operations.

Comment on lines 184 to 187
let _v = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let _v = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
let value = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
drop(value);
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 1);

@b-naber b-naber force-pushed the once_cell branch 2 times, most recently from 78415d1 to 15351a4 Compare March 25, 2021 14:40
@b-naber
Copy link
Contributor Author

b-naber commented Mar 25, 2021

I get an error in CI unrelated to this PR:

error: panic message is not a string literal
   --> tokio/src/time/driver/entry.rs:547:20
    |
547 |             panic!(crate::util::error::RUNTIME_SHUTTING_DOWN_ERROR);
    |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: `-D non-fmt-panic` implied by `-D warnings`
    = note: this is no longer accepted in Rust 2021
help: add a "{}" format string to Display the message
    |
547 |             panic!("{}", crate::util::error::RUNTIME_SHUTTING_DOWN_ERROR);
    |                    ^^^^^
help: or use std::panic::panic_any instead
    |
547 |             std::panic::panic_any(crate::util::error::RUNTIME_SHUTTING_DOWN_ERROR);
    |             ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

error: could not compile `tokio`

@Darksonn
Copy link
Contributor

That must be a new warning with the new version of Rust. You can just apply its first suggestion and it should be ok.

@Darksonn
Copy link
Contributor

Once #3647 is merged, you can fix the CI errors by merging master.

@b-naber
Copy link
Contributor Author

b-naber commented Mar 27, 2021

@Darksonn CI passes now. Thanks for the review and the many suggestions, those were quite helpful.

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks correct. I only have comments on documentation and tests left.

Great work!

Comment on lines 31 to 33
/// let result1 = tokio::spawn(async {
/// ONCE.get_or_init(some_computation).await
/// }).await.unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the example a bit simpler.

Suggested change
/// let result1 = tokio::spawn(async {
/// ONCE.get_or_init(some_computation).await
/// }).await.unwrap();
/// let result1 = ONCE.get_or_init(some_computation).await;

}
}

/// Moves the value out of the cell and drops the cell afterwards.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Moves the value out of the cell and drops the cell afterwards.
/// Moves the value out of the cell, destroying the cell in the process.

impl<T: fmt::Debug> Error for SetError<T> {}

impl<T> SetError<T> {
/// Whether `SetError` is `SetError::AlreadyInitializEderror`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo here. Also, please put a period at the end (also for the other method).

static ONCE: OnceCell<u32> = OnceCell::const_new();

rt.block_on(async {
time::pause();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use start_paused instead.


{
let once_cell = OnceCell::new();
let _ = once_cell.set(fooer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you assert that this set succeeds?

tokio/tests/sync_once_cell.rs Show resolved Hide resolved
@albel727
Copy link

albel727 commented Mar 28, 2021

How about a method for conditional initialization? Sometimes one wants to repeat attempts at initializing until something comes along. The following should pass errors through and leave the cell uninitialized, storing only the first Ok result instead.

    pub async fn get_or_try_init<E, F, Fut>(&self, f: F) -> Result<&T, E>
    where
        F: FnOnce() -> Fut,
        Fut: Future<Output = Result<T, E>>,
    {
        if self.initialized() {
            // SAFETY: once the value is initialized, no mutable references are given out, so
            // we can give out arbitrarily many immutable references
            unsafe { Ok(self.get_unchecked()) }
        } else {
            // After acquire().await we have either acquired a permit while self.value
            // is still uninitialized, or current thread is awoken after another thread
            // has intialized the value and closed the semaphore, in which case self.initialized
            // is true and we don't set the value here
            match self.semaphore.acquire().await {
                Ok(_permit) => {
                    if !self.initialized() {
                        // If `f()` panics or `select!` is called, this `get_or_try_init` call
                        // is aborted and the semaphore permit is dropped.
                        let value = f().await;

                        match value {
                            Ok(value) => {
                                // SAFETY: There is only one permit on the semaphore, hence only one
                                // mutable reference is created
                                unsafe { self.set_value(value) };

                                // SAFETY: once the value is initialized, no mutable references are given out, so
                                // we can give out arbitrarily many immutable references
                                unsafe { Ok(self.get_unchecked()) }
                            },
                            Err(e) => Err(e),
                        }
                    } else {
                        unreachable!("acquired semaphore after value was already initialized.");
                    }
                }
                Err(_) => {
                    if self.initialized() {
                        // SAFETY: once the value is initialized, no mutable references are given out, so
                        // we can give out arbitrarily many immutable references
                        unsafe { Ok(self.get_unchecked()) }
                    } else {
                        unreachable!(
                            "Semaphore closed, but the OnceCell has not been initialized."
                        );
                    }
                }
            }
        }
    }

@Darksonn
Copy link
Contributor

@albel727 Yeah, that would be a reasonable addition as well.

@b-naber
Copy link
Contributor Author

b-naber commented Mar 31, 2021

@Darksonn Updated the PR. Do you want @albel727's suggested method to be included in this PR?

@Darksonn
Copy link
Contributor

It is up to you if you want to do it in this PR.

@b-naber
Copy link
Contributor Author

b-naber commented Apr 1, 2021

@Darksonn Added that method and a test for it. Can you take another look, please?

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all your work. It looks good. 👍

}

let once_cell = OnceCell::new();
let _ = once_cell.set(fooer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please assert that the set succeeds.

Suggested change
let _ = once_cell.set(fooer);
assert!(once_cell.set(fooer).is_ok());

let fooer = once_cell.into_inner();
let count = NUM_DROPS.load(Ordering::Acquire);
assert!(count == 0);
mem::drop(fooer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mem::drop(fooer);
drop(fooer);

@b-naber
Copy link
Contributor Author

b-naber commented Apr 2, 2021

@Darksonn A test not related to this PR fails (tcp_into_std), why is that?

@Darksonn
Copy link
Contributor

Darksonn commented Apr 2, 2021

It failed with "Address already in use". I'll try rerunning the tests.

@b-naber
Copy link
Contributor Author

b-naber commented Apr 3, 2021

@Darksonn thanks for re-running the test. Is there something that still needs to be done before this can be merged?

Copy link
Contributor

@Darksonn Darksonn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a last sanity check, I always like to view the generated documentation and in doing so I found the following. Please also take a look at the generated doc yourself. Besides that, I think this is ready to be merged.

/// If the value of the OnceCell was already set prior to this call
/// then [`SetError::AlreadyInitializedError`] is returned. If another thread
/// is initializing the cell while this method is called,
/// ['SetError::InitializingError`] is returned. In order to wait
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// ['SetError::InitializingError`] is returned. In order to wait
/// [`SetError::InitializingError`] is returned. In order to wait

@b-naber
Copy link
Contributor Author

b-naber commented Apr 5, 2021

@Darksonn Thanks for catching that error. I couldn't find any further errors in the docs myself.

@Darksonn Darksonn merged commit f6e4e85 into tokio-rs:master Apr 5, 2021
@Darksonn
Copy link
Contributor

Darksonn commented Apr 5, 2021

Thanks for all your work on this feature! 🎉

@b-naber
Copy link
Contributor Author

b-naber commented Apr 5, 2021

Thanks for all your help.

@b-naber b-naber deleted the once_cell branch April 5, 2021 20:21
@Darksonn Darksonn mentioned this pull request Apr 12, 2021
@Darksonn Darksonn mentioned this pull request May 5, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-tokio Area: The main tokio crate M-sync Module: tokio/sync
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants