diff --git a/text/3184-thread-local-cell-methods.md b/text/3184-thread-local-cell-methods.md new file mode 100644 index 00000000000..b42df0e730f --- /dev/null +++ b/text/3184-thread-local-cell-methods.md @@ -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>` and `LocalKey>` 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> = 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 = 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>` and `LocalKey>` such that they can used directly without using `.with()`: + +(\* `.get()` only for `Cell`, not for `RefCell`.) + +```rust +thread_local! { + static THINGS: RefCell> = RefCell::new(Vec::new()); +} + +fn f() { + THINGS.set(vec![1, 2, 3]); + + // ... + + let v: Vec = THINGS.take(); +} +``` + +For `.set()`, this *skips the initialization expression*: + +```rust +thread_local! { + static ID: Cell = 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>` to do `.with()` and `.borrow()` or `.borrow_mut()` at once: + +```rust +thread_local! { + static THINGS: RefCell> = 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 LocalKey> { + /// 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 = 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 = 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> = 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 = 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 LocalKey> { + /// 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> = RefCell::new(Vec::new()); + /// } + /// + /// X.with_ref(|v| assert!(v.is_empty())); + /// ``` + pub fn with_ref(&'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> = RefCell::new(Vec::new()); + /// } + /// + /// X.with_mut(|v| v.push(1)); + /// + /// X.with_ref(|v| assert_eq!(*v, vec![1])); + /// ``` + pub fn with_mut(&'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> = 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> = 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> = 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` (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::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>`, that doesn't make a difference, as overwriting the const-initialized value has the same effect. + However, for the generic `LocalKey` 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>::get()` to get the `*mut T`, just like `UnsafeCell::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).