Skip to content

Thread local Cell methods. #3184

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

Merged
merged 8 commits into from
Dec 20, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
399 changes: 399 additions & 0 deletions text/3184-thread-local-cell-methods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,399 @@
- Feature Name: thread_local_cell_methods
- Start Date: 2021-10-17
- RFC PR: [rust-lang/rfcs#3184](https://github.com/rust-lang/rfcs/pull/3184)
- Rust Issue: [rust-lang/rust#92122](https://github.com/rust-lang/rust/issues/92122)

# Summary
[summary]: #summary

Adding methods to `LocalKey` for `LocalKey<Cell<T>>` and `LocalKey<RefCell<T>>` to make thread local cells easier to use.

# Motivation
[motivation]: #motivation

Almost all real-world usages of `thread_local! {}` involve a `Cell` or `RefCell`.
Using the resulting `LocalKey` from a `thread_local! {}` declaration gets verbose due to having to use `.with(|_| ..)`.
(For context: `.with()` is necessary because there's no correct lifetime for the thread local value.
This method makes sure that any borrows end before the thread ends.)

```rust
thread_local! {
static THINGS: RefCell<Vec<i32>> = RefCell::new(Vec::new());
}

fn f() {
THINGS.with(|things| things.borrow_mut().push(1));

// ...

THINGS.with(|things| {
let things = things.borrow();
println!("{:?}", things);
});
}
```

In addition, using `.set()` on a thread local cell through `.with()` results in unnecessary initialization,
since `.with` will trigger the lazy initialization, even though `.set()` will overwrite the value directly afterwards:

```rust
thread_local! {
static ID: Cell<usize> = Cell::new(generate_id());
}

fn f() {
ID.with(|id| id.set(1)); // Ends up calling generate_id() the first time, while ignoring its result.

// ...
}
```

# Proposed additions

We add `.set()`, `.get()`\*, `.take()` and `.replace()` on `LocalKey<Cell<T>>` and `LocalKey<RefCell<T>>` such that they can used directly without using `.with()`:

(\* `.get()` only for `Cell`, not for `RefCell`.)

```rust
thread_local! {
static THINGS: RefCell<Vec<i32>> = RefCell::new(Vec::new());
}

fn f() {
THINGS.set(vec![1, 2, 3]);

// ...

let v: Vec<i32> = THINGS.take();
}
```

For `.set()`, this *skips the initialization expression*:

```rust
thread_local! {
static ID: Cell<usize> = panic!("This thread doesn't have an ID yet!");
}

fn f() {
// ID.with(|id| ..) at this point would panic.

ID.set(123); // This does *not* result in a panic.
}
```

In addition, we add `.with_ref` and `.with_mut` for `LocalKey<RefCell<T>>` to do `.with()` and `.borrow()` or `.borrow_mut()` at once:

```rust
thread_local! {
static THINGS: RefCell<Vec<i32>> = RefCell::new(Vec::new());
}

fn f() {
THINGS.with_mut(|v| v.push(1));

// ...

let len = THINGS.with_ref(|v| v.len());
}
```

# Full reference of the proposed additions

```rust
impl<T: 'static> LocalKey<Cell<T>> {
/// Sets or initializes the contained value.
///
/// Unlike the other methods, this will *not* run the lazy initializer of
/// the thread local. Instead, it will be directly initialized with the
/// given value if it wasn't initialized yet.
///
/// # Panics
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::Cell;
///
/// thread_local! {
/// static X: Cell<i32> = panic!("!");
/// }
///
/// // Calling X.get() here would result in a panic.
///
/// X.set(123); // But X.set() is fine, as it skips the initializer above.
///
/// assert_eq!(X.get(), 123);
/// ```
pub fn set(&'static self, value: T);

/// Returns a copy of the contained value.
///
/// This will lazily initialize the value if this thread has not referenced
/// this key yet.
///
/// # Panics
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::Cell;
///
/// thread_local! {
/// static X: Cell<i32> = Cell::new(1);
/// }
///
/// assert_eq!(X.get(), 1);
/// ```
pub fn get(&'static self) -> T where T: Copy;

/// Takes the contained value, leaving `Default::default()` in its place.
///
/// This will lazily initialize the value if this thread has not referenced
/// this key yet.
///
/// # Panics
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::Cell;
///
/// thread_local! {
/// static X: Cell<Option<i32>> = Cell::new(Some(1));
/// }
///
/// assert_eq!(X.take(), Some(1));
/// assert_eq!(X.take(), None);
/// ```
pub fn take(&'static self) -> T where T: Default;

/// Replaces the contained value, returning the old value.
///
/// This will lazily initialize the value if this thread has not referenced
/// this key yet.
///
/// # Panics
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::Cell;
///
/// thread_local! {
/// static X: Cell<i32> = Cell::new(1);
/// }
///
/// assert_eq!(X.replace(2), 1);
/// assert_eq!(X.replace(3), 2);
/// ```
pub fn replace(&'static self, value: T) -> T;
}
```

```rust
impl<T: 'static> LocalKey<RefCell<T>> {
/// Acquires a reference to the contained value.
///
/// This will lazily initialize the value if this thread has not referenced
/// this key yet.
///
/// # Panics
///
/// Panics if the value is currently borrowed.
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Example
///
/// ```
/// use std::cell::RefCell;
///
/// thread_local! {
/// static X: RefCell<Vec<i32>> = RefCell::new(Vec::new());
/// }
///
/// X.with_ref(|v| assert!(v.is_empty()));
/// ```
pub fn with_ref<F, R>(&'static self, f: F) -> R where F: FnOnce(&T) -> R;

/// Acquires a mutable reference to the contained value.
///
/// This will lazily initialize the value if this thread has not referenced
/// this key yet.
///
/// # Panics
///
/// Panics if the value is currently borrowed.
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Example
///
/// ```
/// use std::cell::RefCell;
///
/// thread_local! {
/// static X: RefCell<Vec<i32>> = RefCell::new(Vec::new());
/// }
///
/// X.with_mut(|v| v.push(1));
///
/// X.with_ref(|v| assert_eq!(*v, vec![1]));
/// ```
pub fn with_mut<F, R>(&'static self, f: F) -> R where F: FnOnce(&mut T) -> R;

/// Sets or initializes the contained value.
///
/// Unlike the other methods, this will *not* run the lazy initializer of
/// the thread local. Instead, it will be directly initialized with the
/// given value if it wasn't initialized yet.
///
/// # Panics
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::RefCell;
///
/// thread_local! {
/// static X: RefCell<Vec<i32>> = panic!("!");
/// }
///
/// // Calling X.with() here would result in a panic.
///
/// X.set(vec![1, 2, 3]); // But X.set() is fine, as it skips the initializer above.
///
/// X.with_ref(|v| assert_eq!(*v, vec![1, 2, 3]));
/// ```
pub fn set(&'static self, value: T);

/// Takes the contained value, leaving `Default::default()` in its place.
///
/// This will lazily initialize the value if this thread has not referenced
/// this key yet.
///
/// # Panics
///
/// Panics if the value is currently borrowed.
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::RefCell;
///
/// thread_local! {
/// static X: RefCell<Vec<i32>> = RefCell::new(Vec::new());
/// }
///
/// X.with_mut(|v| v.push(1));
///
/// let a = X.take();
///
/// assert_eq!(a, vec![1]);
///
/// X.with_ref(|v| assert!(v.is_empty()));
/// ```
pub fn take(&'static self) -> T where T: Default;

/// Replaces the contained value, returning the old value.
///
/// # Panics
///
/// Panics if the value is currently borrowed.
///
/// Panics if the key currently has its destructor running,
/// and it **may** panic if the destructor has previously been run for this thread.
///
/// # Examples
///
/// ```
/// use std::cell::RefCell;
///
/// thread_local! {
/// static X: RefCell<Vec<i32>> = RefCell::new(Vec::new());
/// }
///
/// let prev = X.replace(vec![1, 2, 3]);
/// assert!(prev.is_empty());
///
/// X.with_ref(|v| assert_eq!(*v, vec![1, 2, 3]));
/// ```
pub fn replace(&'static self, value: T) -> T;
}
```

# Drawbacks
[drawbacks]: #drawbacks

- We can no longer use the method names `set`, `get`, etc. on `LocalKey<T>` (if `T` can include `Cell` or `RefCell`).

- It might encourage code that's less efficient on some platforms.
A single `THREAD_LOCAL.with(|x| ..)` is more efficient than using multiple `.set()` and `.get()` (etc.),
since it needs to look up the thread local address every time, which is not free on all platforms.

# Alternatives

Alternatives for making it easier to work with thread local cells:

- Don't do anything, and keep wrapping everything in `.with(|x| ..)`.

- Somehow invent and implement the `'thread` or `'caller` lifetime, removing the need for `.with(|x| ..)`.

- Add `THREAD_LOCAL.borrow()` and `THREAD_LOCAL.borrow_mut()`, just like `RefCell` has.

This wouldn't be sound.
One could move the returned proxy object into a thread local that outlives this thread local.
(Or just `Box::leak()` it.)

Alternatives for avoiding the initializer:

- Add a `LocalKey<T>::try_initialize` method.

- This will be bit more complicated to implement efficiently.
(A `LocalKey` just contains a single function pointer to the thread-local-address-getter, which is often optimized out.
This doesn't play nice with being generic over the initialization function.)

- Thread locals with a `const` initializer (currently unstable, but likely stabilized soon) do not have the concept of being 'uninitialized' and do not run any lazy initialization.
With `.set()` for `LocalKey<Cell<T>>`, that doesn't make a difference, as overwriting the const-initialized value has the same effect.
However, for the generic `LocalKey<T>` we cannot allow changes without internal mutability,
meaning that we can allow initialization (like `.try_initialize()`),
but not changing it later (like `.set()`).
Since a `const` initialized thread local does not know whether its value has been observed yet,
we can't do anything other than implement `.try_initialize()` by always failing or panicking.

- Even if this function existed, it would still be nice to have a simple `THREAD_LOCAL.set(..)`.

# Prior art
[prior-art]: #prior-art

- [`scoped-tls`](https://docs.rs/scoped-tls/1.0.0/scoped_tls/struct.ScopedKey.html)
provides 'scoped thread locals' which must be `.set()` before using them. (They will panic otherwise.)

# Unresolved questions
[unresolved-questions]: #unresolved-questions

- Should we use the names `with_borrow` and `with_borrow_mut` instead of `with_ref` and `with_mut`, to match `RefCell`'s method names?
- Do we also want anything for `UnsafeCell`? Maybe `LocalKey<UnsafeCell<T>>::get()` to get the `*mut T`, just like `UnsafeCell<T>::get()`.
- Are there any other types commonly used as thread locals for which we should do something similar?
- Should `.set` skip the initializer, or not? We should consider this question again at stabilization time, and we should listen for anyone reporting concerns here (especially if it caused semantically unexpected behavior).