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
Add OnceCell #3591
Conversation
@Darksonn Thanks for the review. I addressed your comments. |
@Darksonn Not sure about what to do about the clippy errors in the Edit: In the previous commit I got errors in the clippy pass because the type for the function pointer was 'too complex' in the ? |
Feel free to silence "too complex" errors in the |
ed48b81
to
b6c6d99
Compare
@Darksonn CI passes now. |
tokio/src/sync/once_cell.rs
Outdated
/// 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 |
There was a problem hiding this comment.
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
?
tokio/src/sync/once_cell.rs
Outdated
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 { |
There was a problem hiding this comment.
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()
tokio/src/sync/once_cell.rs
Outdated
let new_cell = OnceCell::new(); | ||
if let Some(value) = self.get() { | ||
match new_cell.set(value.clone()) { | ||
Ok(()) => (), | ||
Err(_) => unreachable!(), | ||
} | ||
} | ||
new_cell |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@Darksonn addressed your comments. |
tokio/src/sync/once_cell.rs
Outdated
|
||
impl<T: Clone> Clone for OnceCell<T> { | ||
fn clone(&self) -> OnceCell<T> { | ||
OnceCell::new_with(self.get().map(|v| (*v).clone())) |
There was a problem hiding this comment.
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.
OnceCell::new_with(self.get().map(|v| (*v).clone())) | |
OnceCell::new_with(self.get().cloned()) |
tokio/src/sync/once_cell.rs
Outdated
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), | ||
} |
There was a problem hiding this comment.
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.
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() | |
} |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
tokio/src/sync/once_cell.rs
Outdated
pub fn into_inner(self) -> Option<T> { | ||
if self.initialized() { | ||
Some(unsafe { self.value.with(|ptr| ptr::read(ptr).assume_init()) }) | ||
} else { | ||
None | ||
} | ||
} |
There was a problem hiding this comment.
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:
- The destructor of the
T
inside is called when the cell is dropped normally. - The destructor of the
T
inside is not called wheninto_inner
is used.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
tokio/src/sync/once_cell.rs
Outdated
if self.initialized() { | ||
let old_me = std::mem::replace(self, OnceCell::new()); | ||
old_me.into_inner() | ||
} else { | ||
None | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be simplified.
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() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mem::take
too
There was a problem hiding this comment.
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.
if self.initialized() { | |
let old_me = std::mem::replace(self, OnceCell::new()); | |
old_me.into_inner() | |
} else { | |
None | |
} | |
std::mem::take(self).into_inner() |
@Darksonn So the new tests don't work for some reason. When edit: actually into_inner doesn't call drop. I didn't know that assigning to a edit2: |
tokio/tests/sync_once_cell.rs
Outdated
let _ = once_cell.into_inner(); | ||
let count = NUM_DROPS.load(Ordering::Acquire); | ||
assert!(count == 0); |
There was a problem hiding this comment.
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.
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); |
We're going to need a destructor, yes. |
@Darksonn can you take another look, please? |
There was a problem hiding this 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.
tokio/src/sync/once_cell.rs
Outdated
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); |
There was a problem hiding this comment.
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:
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.
tokio/tests/sync_once_cell.rs
Outdated
let _v = once_cell.into_inner(); | ||
let count = NUM_DROPS.load(Ordering::Acquire); | ||
assert!(count == 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
78415d1
to
15351a4
Compare
I get an error in CI unrelated to this PR:
|
That must be a new warning with the new version of Rust. You can just apply its first suggestion and it should be ok. |
Once #3647 is merged, you can fix the CI errors by merging master. |
@Darksonn CI passes now. Thanks for the review and the many suggestions, those were quite helpful. |
There was a problem hiding this 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!
tokio/src/sync/once_cell.rs
Outdated
/// let result1 = tokio::spawn(async { | ||
/// ONCE.get_or_init(some_computation).await | ||
/// }).await.unwrap(); |
There was a problem hiding this comment.
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.
/// let result1 = tokio::spawn(async { | |
/// ONCE.get_or_init(some_computation).await | |
/// }).await.unwrap(); | |
/// let result1 = ONCE.get_or_init(some_computation).await; |
tokio/src/sync/once_cell.rs
Outdated
} | ||
} | ||
|
||
/// Moves the value out of the cell and drops the cell afterwards. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// 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. |
tokio/src/sync/once_cell.rs
Outdated
impl<T: fmt::Debug> Error for SetError<T> {} | ||
|
||
impl<T> SetError<T> { | ||
/// Whether `SetError` is `SetError::AlreadyInitializEderror` |
There was a problem hiding this comment.
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).
tokio/tests/sync_once_cell.rs
Outdated
static ONCE: OnceCell<u32> = OnceCell::const_new(); | ||
|
||
rt.block_on(async { | ||
time::pause(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use start_paused
instead.
tokio/tests/sync_once_cell.rs
Outdated
|
||
{ | ||
let once_cell = OnceCell::new(); | ||
let _ = once_cell.set(fooer); |
There was a problem hiding this comment.
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?
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."
);
}
}
}
}
} |
@albel727 Yeah, that would be a reasonable addition as well. |
It is up to you if you want to do it in this PR. |
@Darksonn Added that method and a test for it. Can you take another look, please? |
There was a problem hiding this 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. 👍
tokio/tests/sync_once_cell.rs
Outdated
} | ||
|
||
let once_cell = OnceCell::new(); | ||
let _ = once_cell.set(fooer); |
There was a problem hiding this comment.
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.
let _ = once_cell.set(fooer); | |
assert!(once_cell.set(fooer).is_ok()); |
tokio/tests/sync_once_cell.rs
Outdated
let fooer = once_cell.into_inner(); | ||
let count = NUM_DROPS.load(Ordering::Acquire); | ||
assert!(count == 0); | ||
mem::drop(fooer); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mem::drop(fooer); | |
drop(fooer); |
@Darksonn A test not related to this PR fails ( |
It failed with "Address already in use". I'll try rerunning the tests. |
@Darksonn thanks for re-running the test. Is there something that still needs to be done before this can be merged? |
There was a problem hiding this 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.
tokio/src/sync/once_cell.rs
Outdated
/// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// ['SetError::InitializingError`] is returned. In order to wait | |
/// [`SetError::InitializingError`] is returned. In order to wait |
@Darksonn Thanks for catching that error. I couldn't find any further errors in the docs myself. |
Thanks for all your work on this feature! 🎉 |
Thanks for all your help. |
Separating #3513 into two PRs for clarity, as requested by @Darksonn. Since
Lazy
depends onOnceCell
, a PR forLazy
will be opened once this PR lands.