Summary

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

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.)


#![allow(unused)]
fn main() {
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:


#![allow(unused)]
fn main() {
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.)


#![allow(unused)]
fn main() {
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:


#![allow(unused)]
fn main() {
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:


#![allow(unused)]
fn main() {
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


#![allow(unused)]
fn main() {
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;
}
}

#![allow(unused)]
fn main() {
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

  • 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

  • scoped-tls provides 'scoped thread locals' which must be .set() before using them. (They will panic otherwise.)

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).