Skip to content

std::thread::LocalKeyState: Add fourth Initializing state #43491

Closed
@joshlf

Description

@joshlf

Right now, LocalKeyState contains three states: Uninitialized, Valid, and Destroyed.

Unfortunately, this is not enough to cover all cases. In particular, consider the following code (adapted from some code I wrote as part of writing a global allocator - to motivate that this is a real-world example):

fn local_or_global(foo: Foo) -> Bar {
    match LOCAL.state() {
        LocalKeyState::Uninitialized | LocalKeyState::Valid => LOCAL.with(|local| do_computation(foo, local)),
        LocalKeyState::Destroyed => do_computation(foo, &GLOBAL),
    }
}

The idea here is to ensure that the local key hasn't yet been destroyed - and if it has, to fall back on some global state instead. If the key is valid, then obviously this is fine. If the key is uninitialized, then with should perform initialization, which should also be fine...

unless local_or_global is called from the initialization routine. This is the case for the allocator code I'm working on since initializing TLS requires performing allocations. In that case, you end up with infinite recursion and eventually a stack overflow.

My proposal is the following: add a fourth Initializing state. When with is first called and the key needs to be initialized, it is first moved into the Initializing state. When initialization completes, it is subsequently moved into the Valid state. This allows code that accesses the key to detect whether it's being called from inside the initializer, and behave appropriately in that case (in particular, by not accessing the key and thus recursing the initialization).

The concrete issue I (along with my collaborator on this project, @ezrosent) have run into is that initializing TLS depends on allocation, which depends on TLS, which depends on... In general, this is a problem whenever a TLS key's initialization routine transitively depends on itself (either directly, or through an arbitrarily long cycle of other TLS key dependencies - for example, we've seen similar issues with Destroyed keys when using Crossbeam's epoch-based memory reclamation, which also uses TLS under the hood).

Alternatives

It might be tempting to think that just making sure you're in the Valid state is a sufficient (if sub-optimal) solution to this problem (and in fact, I thought the same thing for a while). Unfortunately, this solves a chicken-and-egg problem by just deciding that chickens can't exist. By only being willing to access a TLS key in the Valid state, you preclude ever calling code that will perform initialization, and thus you'll never move to the Valid state in the first place.

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-feature-requestCategory: A feature request, i.e: not implemented / a PR.T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions