Skip to content

Static thread_local! declarations are moved before Drop is called #140816

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
orlp opened this issue May 8, 2025 · 7 comments
Open

Static thread_local! declarations are moved before Drop is called #140816

orlp opened this issue May 8, 2025 · 7 comments
Labels
A-thread-locals Area: Thread local storage (TLS) C-discussion Category: Discussion or questions that doesn't represent real issues. C-feature-request Category: A feature request, i.e: not implemented / a PR. needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@orlp
Copy link
Contributor

orlp commented May 8, 2025

Consider this piece of code:

use std::sync::atomic::*;

static UNIQ_ID: AtomicU64 = AtomicU64::new(0);

struct Foo(u64);

impl Foo {
    fn new() -> Foo {
        Foo(UNIQ_ID.fetch_add(1, Ordering::Relaxed))
    }
}

impl Drop for Foo {
    fn drop(&mut self) {
        eprintln!("{} dropped with addr {:p}", self.0, self);
    }
}

fn main() {
    thread_local!(static FOO: Foo = Foo::new());
    FOO.with(|foo| {
        eprintln!("{} living with addr {:p}", foo.0, foo);
    });
}

I would expect the statically declared FOO to not move around after initialization. In fact, Foo might be a type which may not be moved around after initialization at all, such as a type containing pthread_mutex_t. However, we see that it does get moved before the drop:

0 living with addr 0x6000002000a8
0 dropped with addr 0x16f9faaa8

I believe the issue lies here:

// Update the state before running the destructor as it may attempt to
// access the variable.
let val = unsafe { storage.state.get().replace(State::Destroyed(())) };
drop(val);

This code should be changed to not use an enum but rather a separate state flag + cell, so that the value is not moved.

@orlp orlp added the C-bug Category: This is a bug. label May 8, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label May 8, 2025
@jieyouxu jieyouxu added T-libs Relevant to the library team, which will review and decide on the PR/issue. A-thread-locals Area: Thread local storage (TLS) labels May 8, 2025
@Amanieu
Copy link
Member

Amanieu commented May 8, 2025

cc @joboet

@joboet joboet added C-feature-request Category: A feature request, i.e: not implemented / a PR. C-discussion Category: Discussion or questions that doesn't represent real issues. and removed C-bug Category: This is a bug. labels May 12, 2025
@joboet
Copy link
Member

joboet commented May 12, 2025

std doesn't promise immovability for thread locals anywhere, so this isn't a bug. Still, I think asking for such a promise isn't totally unreasonable. When I rewrote the TLS implementation, I thought that the niche optimisation made possible by using an enum would be more valuable than immovability, especially since I still think that it wouldn't be possible to use LocalKey together with Pin – but I don't feel strongly.

@Amanieu
Copy link
Member

Amanieu commented May 13, 2025

The specific issue that triggered this is that parking_lot keeps a pthread_mutex_t in a thread-local which cannot be moved after it has been initialized (miri checks and enforces this).

@kpreid
Copy link
Contributor

kpreid commented May 13, 2025

When I rewrote the TLS implementation, I thought that the niche optimisation made possible by using an enum would be more valuable than immovability, …

An argument for immovability:

The comparison that comes to mind here is that for every instance of a thread-local variable, there is a thread, and threads have stacks, which in most programs have to be generously oversized for reliability, and adding a single byte discriminant seems insignificant compared to that. Is thread-local storage space especially limited in some way, such that that byte is more valuable here?

An argument for the status quo:

That byte is a cost to all thread-locals, and applications that need the value not to move can use Box<T> (or even Pin<Box<T>>) instead.

@orlp

This comment has been minimized.

@orlp

This comment has been minimized.

@orlp
Copy link
Contributor Author

orlp commented May 13, 2025

@kpreid Sorry my previous claim we could get the best of both worlds doesn't work. I think just paying the 1 byte per thread-local penalty is the best solution.

Then again, the 1-byte penalty might not actually be 1 byte after alignment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-thread-locals Area: Thread local storage (TLS) C-discussion Category: Discussion or questions that doesn't represent real issues. C-feature-request Category: A feature request, i.e: not implemented / a PR. needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

6 participants