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.)
thread_local!
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:
thread_local!
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.)
thread_local!
For .set(), this skips the initialization expression:
thread_local!
In addition, we add .with_ref and .with_mut for LocalKey<RefCell<T>> to do .with() and .borrow() or .borrow_mut() at once:
thread_local!
Full reference of the proposed additions
Drawbacks
-
We can no longer use the method names
set,get, etc. onLocalKey<T>(ifTcan includeCellorRefCell). -
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
'threador'callerlifetime, removing the need for.with(|x| ..). -
Add
THREAD_LOCAL.borrow()andTHREAD_LOCAL.borrow_mut(), just likeRefCellhas.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_initializemethod.-
This will be bit more complicated to implement efficiently. (A
LocalKeyjust 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
constinitializer (currently unstable, but likely stabilized soon) do not have the concept of being 'uninitialized' and do not run any lazy initialization. With.set()forLocalKey<Cell<T>>, that doesn't make a difference, as overwriting the const-initialized value has the same effect. However, for the genericLocalKey<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 aconstinitialized 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-tlsprovides 'scoped thread locals' which must be.set()before using them. (They will panic otherwise.)
Unresolved questions
- Should we use the names
with_borrowandwith_borrow_mutinstead ofwith_refandwith_mut, to matchRefCell's method names? - Do we also want anything for
UnsafeCell? MaybeLocalKey<UnsafeCell<T>>::get()to get the*mut T, just likeUnsafeCell<T>::get(). - Are there any other types commonly used as thread locals for which we should do something similar?
- Should
.setskip 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).