Skip to content

Commit 93231eb

Browse files
tgross35bl-ue
andauthored
Apply suggestions from tgross35's code review
- Update LazyCell/LazyLock/OnceCell/OnceLock usage - Clarify stance on atomics - Some minor flow updates Co-authored-by: bl-ue <54780737+bl-ue@users.noreply.github.com>
1 parent 2b31697 commit 93231eb

File tree

1 file changed

+30
-45
lines changed

1 file changed

+30
-45
lines changed

text/0000-standard-lazy-types.md

Lines changed: 30 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ At the same time, working with lazy values in Rust is not easy:
3030
* C++ and Java provide language-level delayed initialization for static values, while Rust requires explicit code to handle runtime-initialization.
3131
* Rust borrowing rules require a special pattern when implementing lazy fields.
3232

33-
While `lazy_static` is implemented using macros, to work-around language limitations, today it is possible to implement similar functionality without resorting to macros, as a natural combination of two features:
34-
* lazy values
35-
* `static` keyword
33+
`lazy_static` is implemented using macros, to work-around former language limitations. Since then, various language improvements have made it possible to create runtime initialized (lazy) objects in a `static` scope, accomplishing the same goals without macros.
3634

3735
We can have a single canonical API for a commonly used tricky unsafe concept, so we probably should have it!
3836

@@ -104,7 +102,7 @@ Notable features of the API:
104102
Similarly to other interior mutability primitives, `OnceCell` comes in two flavors:
105103

106104
* Non thread-safe `std::cell::OnceCell`.
107-
* Thread-safe `std::sync::OnceCell`.
105+
* Thread-safe `std::sync::OnceLock`.
108106

109107
Here's how `OnceCell` can be used to implement lazy-initialized global data:
110108

@@ -142,37 +140,35 @@ impl Ctx {
142140
}
143141
```
144142

145-
We also provide a more convenient but less powerful `Lazy<T, F>` wrapper around `OnceCell<T>`, which allows to specify the initializing closure at creation time:
143+
We also provide the more convenient but less powerful `Lazy<T, F>` and `LazyLock<T, F>` wrappers around `OnceCell<T>` and `OnceLock<T>`, which allows specifying the initializing closure at creation time:
146144

147145
```rust
148-
pub struct Lazy<T, F = fn() -> T> { ... }
146+
pub struct LazyCell<T, F = fn() -> T> { ... }
149147

150-
impl<T, F> Lazy<T, F> {
148+
impl<T, F: FnOnce() -> T> LazyCell<T, F> {
151149
/// Creates a new lazy value with the given initializing function.
152-
pub const fn new(init: F) -> Lazy<T, F>;
153-
}
154-
155-
impl<T, F: FnOnce() -> T> Lazy<T, F> {
150+
pub const fn new(init: F) -> LazyCell<T, F>;
151+
156152
/// Forces the evaluation of this lazy value and returns a reference to
157153
/// the result.
158154
///
159155
/// This is equivalent to the `Deref` impl, but is explicit.
160-
pub fn force(this: &Lazy<T, F>) -> &T;
156+
pub fn force(this: &LazyCell<T, F>) -> &T;
161157
}
162158

163-
impl<T, F: FnOnce() -> T> Deref for Lazy<T, F> {
159+
impl<T, F: FnOnce() -> T> Deref for LazyCell<T, F> {
164160
type Target = T;
165161

166162
fn deref(&self) -> &T;
167163
}
168164
```
169165

170-
`Lazy` directly replaces `lazy_static!`:
166+
`LazyLock` directly replaces `lazy_static!`:
171167

172168
```rust
173-
use std::{sync::{Mutex, Lazy}, collections::HashMap};
169+
use std::{sync::{Mutex, LazyLock}, collections::HashMap};
174170

175-
static GLOBAL_DATA: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| {
171+
static GLOBAL_DATA: LazyLock<Mutex<HashMap<i32, String>>> = LazyLock::new(|| {
176172
let mut m = HashMap::new();
177173
m.insert(13, "Spica".to_string());
178174
m.insert(74, "Hoyten".to_string());
@@ -189,16 +185,15 @@ use std::cell::{RefCell, Lazy};
189185
pub static FOO: Lazy<RefCell<u32>> = Lazy::new(|| RefCell::new(1));
190186
```
191187

192-
However, `#[thread_local]` attribute is pretty far from stabilization at the moment, and due to the required special handling of destructors, it's unclear if just using `cell::Lazy` will work out.
193188

194189
Unlike `lazy_static!`, `Lazy` can be used for locals:
195190

196191
```rust
197-
use std::cell::Lazy;
192+
use std::cell::LazyCell;
198193

199194
fn main() {
200195
let ctx = vec![1, 2, 3];
201-
let thunk = Lazy::new(|| {
196+
let thunk = LazyCell::new(|| {
202197
ctx.iter().sum::<i32>()
203198
});
204199
assert_eq!(*thunk, 6);
@@ -212,12 +207,12 @@ The proposed API is directly copied from [`once_cell`] crate.
212207

213208
Altogether, this RFC proposes to add four types:
214209

215-
* `std::cell::OnceCell`, `std::cell::Lazy`
216-
* `std::sync::OnceCell`, `std::sync::Lazy`
210+
* `std::cell::OnceCell`, `std::cell::LazyCell`
211+
* `std::sync::OnceLock`, `std::sync::LazyLock`
217212

218-
`OnceCell` is an important core primitive.
219-
`Lazy` can be stabilized separately from `OnceCell`, or it can be omitted from the standard library altogether.
220-
However, it provides significantly nicer ergonomics for the common use-case of static lazy values.
213+
`OnceCell` and `OnceLock` are important primitives.
214+
`LazyCell ` and `LazyLock` can be stabilized separately from `OnceCell`, or optionally omitted from the standard library altogether.
215+
However, as they provide significantly nicer ergonomics for the common use case of static lazy values, it is worth developing in tandem.
221216

222217
Non thread-safe flavor is implemented by storing an `UnsafeCell<Option<T>>`:
223218

@@ -247,27 +242,27 @@ Non thread-safe flavor can be added to `core` as well.
247242

248243
The thread-safe variant is implemented similarly to `std::sync::Once`.
249244
Crucially, it has support for blocking: if many threads call `get_or_init` concurrently, only one will be able to execute the closure, while all other threads will block.
250-
For this reason, most of `std::sync::OnceCell` API can not be provided in `core`.
245+
For this reason, most of `std::sync::OnceLock` API can not be provided in `core`.
251246
In the `sync` case, reliably panicking on re-entrant initialization is not trivial.
252247
For this reason, the implementation would simply deadlock, with a note that a deadlock might be elevated to a panic in the future.
253248

254249
# Drawbacks
255250
[drawbacks]: #drawbacks
256251

257252
* This is a moderately large addition to stdlib, there's a chance we do something wrong.
258-
This can be mitigated by piece-wise stabilization (in particular, `Lazy` convenience types are optional) and the fact that API is tested in the crates.io ecosystem via `once_cell` crate.
253+
This can be mitigated by piece-wise stabilization (in particular, `LazyCell` convenience types are optional) and the fact that API is tested in the crates.io ecosystem via `once_cell` crate.
259254

260-
* The design of `Lazy` type uses default type-parameter as a work-around for the absence of type-inference of statics.
255+
* The design of `LazyCell` type uses default type-parameter as a workaround for the absence of type inference of statics.
261256

262257
* We use the same name for unsync and sync types, which might be confusing.
263258

264259
# Rationale and alternatives
265260
[rationale-and-alternatives]: #rationale-and-alternatives
266261

267-
## Why not `Lazy` as a primitive?
262+
## Why not `LazyCell` as a primitive?
268263

269-
On the first look, it may seem like we don't need `OnceCell`, and should only provide `Lazy`.
270-
The critical drawback of `Lazy` is that it's not always possible to provide the closure at creation time.
264+
On the first look, it may seem like we don't need `OnceCell`, and should only provide `LazyCell`.
265+
The critical drawback of `LazyCell` is that it's not always possible to provide the closure at creation time.
271266

272267
This is important for lazy fields:
273268

@@ -361,12 +356,12 @@ fn try_set(&self, value: T) -> Result<&T, (Option<&T>, T)>
361356
```
362357

363358
That is, if value is set successfully, a reference is returned.
364-
Otherwise, ther the cell is either fully initialized, and a reference is returned as well, or the cell is being initialized, and no valid reference exist yet.
359+
Otherwise, the cell is either fully initialized, and a reference is returned as well, or the cell is being initialized, and no valid reference exist yet.
365360

366361
## Support for `no_std`
367362

368-
The RFC proposes to add `cell::OnceCell` and `cell::Lazy` to `core`, while keeping `sync::OnceCell` and `sync::Lazy` `std`-only.
369-
However, there's a subset of `sync::OnceCell` that can be provided in `core`:
363+
The RFC proposes to add `cell::OnceCell` and `cell::LazyCell` to `core`, while keeping `sync::OnceLock` and `sync::LazyLock` `std`-only.
364+
However, there's a subset of `OnceLock` that can be provided in `core`:
370365

371366
```rust
372367
impl<T> OnceCell<T> {
@@ -464,17 +459,7 @@ However, the situation with `consume` ordering is cloudy right now:
464459
* [nobody knows what it actually means](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0371r0.html),
465460
* [but people rely on it in practice for performance](https://docs.rs/crossbeam-utils/0.7.0/crossbeam_utils/atomic/trait.AtomicConsume.html#tymethod.load_consume).
466461

467-
We can do one of the following:
468-
469-
1. Specify and implement `acquire` ordering,
470-
2. Specify `consume` but implement `acquire` (or hack `consume` in an implementation-defined manner) with the hope to make implementation more efficient later.
471-
3. Specify and implement `acquire`, but provide additional API which can take `Ordering` as an argument.
472-
473-
Option two seems the most promising:
474-
475-
* it is forward compatible with specifying `acquire` later,
476-
* for typical `OnceCell` use-cases, `consume` should be enough.
477-
For guaranteeing side effects, `std::sync::Once` may be used instead.
462+
Given the cost of `consume` ordering for minimal benefit, this crate proposes to specify and implement `acquire/release` ordering. If at some point Rust adds a `consume/release` option to `std::sync::atomic::Ordering`, the option of adding API methods that accept an `Ordering` can be considered.
478463

479464
# Prior art
480465
[prior-art]: #prior-art
@@ -495,7 +480,7 @@ This design doesn't always work in Rust, as closing over `self` runs afoul of th
495480
[unresolved-questions]: #unresolved-questions
496481

497482
- What is the best naming/place for these types?
498-
- What is the best naming scheme for methods? Is it `get_or_try_init` or `try_inert_with`?
483+
- What is the best naming scheme for methods? Is it `get_or_try_init` or `try_insert_with`?
499484
- Is the `F = fn() -> T` hack worth it?
500485
- Which synchronization guarantee should we pick?
501486

0 commit comments

Comments
 (0)