RFC 0461: tls-overhaul

libs (threads)

Summary

Introduce a new thread local storage module to the standard library, std::tls, providing:

Motivation

In the past, the standard library's answer to thread local storage was the std::local_data module. This module was designed based on the Rust task model where a task could be either a 1:1 or M:N task. This design constraint has since been lifted, allowing for easier solutions to some of the current drawbacks of the module. While redesigning std::local_data, it can also be scrutinized to see how it holds up to modern-day Rust style, guidelines, and conventions.

In general the amount of work being scheduled for 1.0 is being trimmed down as much as possible, especially new work in the standard library that isn't focused on cutting back what we're shipping. Thread local storage, however, is such a critical part of many applications and opens many doors to interesting sets of functionality that this RFC sees fit to try and wedge it into the schedule. The current std::local_data module simply doesn't meet the requirements of what one may expect out of a TLS implementation for a language like Rust.

Current Drawbacks

Today's implementation of thread local storage, std::local_data, suffers from a few drawbacks:

Current Strengths

There are, however, a few pros to the usage of the module today which should be required for any replacement:

Building blocks available

There are currently two primary building blocks available to Rust when building a thread local storage abstraction, #[thread_local] and OS-based TLS. Neither of these are currently used for std::local_data, but are generally seen as "adequately efficient" implementations of TLS. For example, an TLS access of a #[thread_local] global is simply a pointer offset, which when compared to a O(log N) lookup is quite speedy!

With these available, this RFC is motivated in redesigning TLS to make use of these primitives.

Detailed design

Three new modules will be added to the standard library:

The design described below can be found as an existing cargo package: https://github.com/alexcrichton/tls-rs.

The OS layer

While LLVM has support for #[thread_local] statics, this feature is not supported on all platforms that LLVM can target. Almost all platforms, however, provide some form of OS-based TLS. For example Unix normally comes with pthread_key_create while Windows comes with TlsAlloc.

This RFC proposes introducing a std::sys::tls module which contains bindings to the OS-based TLS mechanism. This corresponds to the os module in the example implementation. While not currently public, the contents of sys are slated to become public over time, and the API of the std::sys::tls module will go under API stabilization at that time.

This module will support "statically allocated" keys as well as dynamically allocated keys. A statically allocated key will actually allocate a key on first use.

Destructor support

The major difference between Unix and Windows TLS support is that Unix supports a destructor function for each TLS slot while Windows does not. When each Unix TLS key is created, an optional destructor is specified. If any key has a non-NULL value when a thread exits, the destructor is then run on that value.

One possibility for this std::sys::tls module would be to not provide destructor support at all (least common denominator), but this RFC proposes implementing destructor support for Windows to ensure that functionality is not lost when writing Unix-only code.

Destructor support for Windows will be provided through a custom implementation of tracking known destructors for TLS keys.

Scoped TLS

As discussed before, one of the motivations for this RFC is to provide a method of inserting any value into TLS, not just those that ascribe to 'static. This provides maximal flexibility in storing values into TLS to ensure any "thread local" pattern can be encompassed.

Values which do not adhere to 'static contain references with a constrained lifetime, and can therefore not be moved into TLS. They can, however, be borrowed by TLS. This scoped TLS api provides the ability to insert a reference for a particular period of time, and then a non-escaping reference can be extracted at any time later on.

In order to implement this form of TLS, a new module, std::tls::scoped, will be added. It will be coupled with a scoped_tls! macro in the prelude. The API looks like:

/// Declares a new scoped TLS key. The keyword `static` is required in front to
/// emphasize that a `static` item is being created. There is no initializer
/// expression because this key initially contains no value.
///
/// A `pub` variant is also provided to generate a public `static` item.
macro_rules! scoped_tls(
    (static $name:ident: $t:ty) => (/* ... */);
    (pub static $name:ident: $t:ty) => (/* ... */);
)

/// A structure representing a scoped TLS key.
///
/// This structure cannot be created dynamically, and it is accessed via its
/// methods.
pub struct Key<T> { /* ... */ }

impl<T> Key<T> {
    /// Insert a value into this scoped TLS slot for a duration of a closure.
    ///
    /// While `cb` is running, the value `t` will be returned by `get` unless
    /// this function is called recursively inside of cb.
    ///
    /// Upon return, this function will restore the previous TLS value, if any
    /// was available.
    pub fn set<R>(&'static self, t: &T, cb: || -> R) -> R { /* ... */ }

    /// Get a value out of this scoped TLS variable.
    ///
    /// This function takes a closure which receives the value of this TLS
    /// variable, if any is available. If this variable has not yet been set,
    /// then None is yielded.
    pub fn with<R>(&'static self, cb: |Option<&T>| -> R) -> R { /* ... */ }
}

The purpose of this module is to enable the ability to insert a value into TLS for a scoped period of time. While able to cover many TLS patterns, this flavor of TLS is not comprehensive, motivating the owning variant of TLS.

Variations

Specifically the with API can be somewhat unwieldy to use. The with function takes a closure to run, yielding a value to the closure. It is believed that this is required for the implementation to be sound, but it also goes against the "use RAII everywhere" principle found elsewhere in the stdlib.

Additionally, the with function is more commonly called get for accessing a contained value in the stdlib. The name with is recommended because it may be possible in the future to express a get function returning a reference with a lifetime bound to the stack frame of the caller, but it is not currently possible to do so.

The with functions yields an Option<&T> instead of &T. This is to cover the use case where the key has not been set before it used via with. This is somewhat unergonomic, however, as it will almost always be followed by unwrap(). An alternative design would be to provide a is_set function and have with panic! instead.

Owning TLS

Although scoped TLS can store any value, it is also limited in the fact that it cannot own a value. This means that TLS values cannot escape the stack from from which they originated from. This is itself another common usage pattern of TLS, and to solve this problem the std::tls module will provided support for placing owned values into TLS.

These values must not contain references as that could trigger a use-after-free, but otherwise there are no restrictions on placing statics into owned TLS. The module will support dynamic initialization (run on first use of the variable) as well as dynamic destruction (implementors of Drop).

The interface provided will be similar to what std::local_data provides today, except that the replace function has no analog (it would be written with a RefCell<Option<T>>).

/// Similar to the `scoped_tls!` macro, except allows for an initializer
/// expression as well.
macro_rules! tls(
    (static $name:ident: $t:ty = $init:expr) => (/* ... */)
    (pub static $name:ident: $t:ty = $init:expr) => (/* ... */)
)

pub struct Key<T: 'static> { /* ... */ }

impl<T: 'static> Key<T> {
    /// Access this TLS variable, lazily initializing it if necessary.
    ///
    /// The first time this function is called on each thread the TLS key will
    /// be initialized by having the specified init expression evaluated on the
    /// current thread.
    ///
    /// This function can return `None` for the same reasons of static TLS
    /// returning `None` (destructors are running or may have run).
    pub fn with<R>(&'static self, f: |Option<&T>| -> R) -> R { /* ... */ }
}

Destructors

One of the major points about this implementation is that it allows for values with destructors, meaning that destructors must be run when a thread exits. This is similar to placing a value with a destructor into std::local_data. This RFC attempts to refine the story around destructors:

These semantics are still a little unclear, and the final behavior may still need some more hammering out. The sample implementation suffers from a few extra drawbacks, but it is believed that some more implementation work can overcome some of the minor downsides.

Variations

Like the scoped TLS variation, this key has a with function instead of the normally expected get function (returning a reference). One possible alternative would be to yield &T instead of Option<&T> and panic! if the variable has been destroyed. Another possible alternative is to have a get function returning a Ref<T>. Currently this is unsafe, however, as there is no way to ensure that Ref<T> does not satisfy 'static. If the returned reference satisfies 'static, then it's possible for TLS values to reference each other after one has been destroyed, causing a use-after-free.

Drawbacks

Alternatives

Alternatives on the API can be found in the "Variations" sections above.

Some other alternatives might include:

Unresolved questions