-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3184 from m-ou-se/thread-local-cell-methods
Thread local Cell methods.
- Loading branch information
Showing
1 changed file
with
399 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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). |