Tracking issue for thread_local stabilization
The #[thread_local] attribute is currently feature-gated. This issue tracks its stabilization.
Known problems:
- [ ]
#[thread_local]translates directly to thethread_localattribute in LLVM. This isn't supported on all platforms, and it's not even supported on all distributions within the same platform (e.g. 10.6 doesn't support it but 10.7 does). I don't think this is necessarily a blocker, but I also don't think we have many attributes and such which are so platform specific like this. - [x] Statics that are thread local shouldn't require
Sync- https://github.com/rust-lang/rust/issues/18001 - [x] Statics that are thread local should either not borrow for the
'staticlifetime or should be unsafe to access - https://github.com/rust-lang/rust/issues/17954 - [x] Statics can currently reference other thread local statics, but this is a bug - https://github.com/rust-lang/rust/issues/18712
- [ ] Unsound with generators https://github.com/rust-lang/rust/issues/49682
- [ ] static mut can be given
'staticlifetime with NLL (https://github.com/rust-lang/rust/issues/54366)
The
#[thread_local]attribute is currently feature-gated. This issue tracks its stabilization.Known problems:
- [ ]
#[thread_local]translates directly to thethread_localattribute in LLVM. This isn't supported on all platforms, and it's not even supported on all distributions within the same platform (e.g. macOS 10.6 didn't support it but 10.7 does). I don't think this is necessarily a blocker, but I also don't think we have many attributes and such which are so platform specific like this. - [x] Statics that are thread local shouldn't require
Sync- https://github.com/rust-lang/rust/issues/18001 - [x] Statics that are thread local should either not borrow for the
'staticlifetime or should be unsafe to access - https://github.com/rust-lang/rust/issues/17954 - [x] Statics can currently reference other thread local statics, but this is a bug - https://github.com/rust-lang/rust/issues/18712
- [ ] Unsound with generators https://github.com/rust-lang/rust/issues/49682
- [ ] static mut can be given
'staticlifetime with NLL (https://github.com/rust-lang/rust/issues/54366)
Ralf Jung at 2024-08-28 06:10:19
- [ ]
@alexcrichton Can you elaborate on the blockers?
Aaron Turon at 2015-11-05 00:39:52
Certainly! Known issues to me:
#[thread_local]translates directly to thethread_localattribute in LLVM. This isn't supported on all platforms, and it's not even supported on all distributions within the same platform (e.g. 10.6 doesn't support it but 10.7 does). I don't think this is necessarily a blocker, but I also don't think we have many attributes and such which are so platform specific like this.- Statics that are thread local shouldn't require
Sync- https://github.com/rust-lang/rust/issues/18001 - Statics that are thread local should either not borrow for the
'staticlifetime or should be unsafe to access - https://github.com/rust-lang/rust/issues/17954 - Statics can currently reference other thread local statics, but this is a bug - https://github.com/rust-lang/rust/issues/18712
That's at least what I can think of at this time!
Alex Crichton at 2015-11-05 00:55:12
A note, we've since implemented
cfg(target_thread_local)which is in turn itself feature gated, but this may ease the "this isn't implemented on all platforms" worry.Alex Crichton at 2016-02-17 07:39:12
Hi! Is there any update on the status? Nightly still requires statics to be
Sync. I tried with:rustc 1.13.0-nightly (acd3f796d 2016-08-28) binary: rustc commit-hash: acd3f796d26e9295db1eba1ef16e0d4cc3b96dd5 commit-date: 2016-08-28 host: x86_64-unknown-linux-gnu release: 1.13.0-nightlyAlexander Merritt at 2016-08-30 16:56:25
@alexcrichton Any news on
#[thread_local]becoming stabilized? AFAIK, at the moment it is impossible on DragonFly to accesserrnovariable from stable code, other than directly from libstd. This blocks crates likenixon DragonFly, which want to access errno as well, but libstd is not exposing it, and stable code is not allowed to use feature(thread_local).Michael Neumann at 2017-07-10 11:08:56
@mneumann no, no progress. I'd recommend a C shim for now.
Alex Crichton at 2017-07-10 14:03:29
@alexcrichton thanks. I am doing a shim now https://github.com/mneumann/errno-dragonfly-rs.
Michael Neumann at 2017-07-10 14:39:04
The optimizations are too aggressive ;)
See this code:
#![feature(thread_local)] #[thread_local] pub static FOO: [&str; 1] = [ "Hello" ]; fn change_foo(s: &'static str) { FOO[0] = s; } fn main() { println!("{}", FOO[0]); change_foo("Test"); println!("{}", FOO[0]); }The compiler does not detect the side effect in
change_fooand removes the call in release. The output is:Hello HelloFélix at 2017-09-08 18:18:48
cc @eddyb, @Boiethios your example shouldn't actually compile because it should require
static mut, not juststaticAlex Crichton at 2017-09-08 18:24:53
It compiles with the last nightly rustc.
Félix at 2017-09-08 18:27:22
Oh, drat, this is from my shortening of the lifetime, i.e https://github.com/rust-lang/rust/blob/dead08cb331343b84564628b139b657f93548320/src/librustc/middle/mem_categorization.rs#L657-L662 @nikomatsakis what should we do here? I want a static lvalue, with a non-
'staticlifetime.Eduard-Mihai Burtescu at 2017-09-08 18:34:28
#18001, #17954 and https://github.com/rust-lang/rust/issues/18712 were fixed by https://github.com/rust-lang/rust/pull/43746.
@alexcrichton @eddyb Do you know any other blockers, or is this ready for stabilization?
Taylor Cramer at 2018-01-17 01:59:25
There's some emulation
clangdoes IIRC, that we might want to do ourselves, to support#[thread_local]everywhere.And there's #47053 which results from my initial attempt to limit references to thread-local statics to the function they were created in.
Eduard-Mihai Burtescu at 2018-01-17 05:44:06
@cramertj I've personally been under the impression that we're holding out on stabilizing this for as long as possible. We've stabilized very few (AFAIK) platform-specific attributes like this and I at least personally haven't ever looked to hard into stabilizing this.
One blocker (in my mind at least) is what @eddyb mentioned where this is a "portable" attribute yet LLVM has a bunch of emulation on targets that don't actually support it (I think MinGW is an example). I don't believe we should allow the attribute on such targets, but we'd have to do a lot of investigation to figure out those targets.
Is there motivation for stabilizing this though? That'd certainly provide some good motivation for digging out any remaining issues and looking at it very closely. I'd just personally been under the impression that there's little motivation to stabilize this other than it'd be a "nice to have" in situations here and there.
Alex Crichton at 2018-01-17 14:56:18
I am using
#[thread_local]in my code in ano_stdcontext: I allocate space for the TLS segment and set up the architecture TLS register to point to it. Therefore I think that it is important to expose the#[thread_local]attribute so that it can at least be used by low-level code.The only thing that I'm not too happy about is that
Syncis no longer required for#[thread_local]: this makes it more difficult to write signal-safe code, since all communication between signal handlers and the main thread should be done through atomic types.Amanieu d'Antras at 2018-01-17 15:32:57
@Amanieu Signal/interrupt-safe code has other requirements which are not satisfied by current Rust.
Eduard-Mihai Burtescu at 2018-01-17 17:34:02
@eddyb Not really, all you need to do is treat the signal handler as a separate thread of execution. The only requirement you need to enforce safety is that objects which are accessed by both the main thread and the signal handler must be
Sync.Of course you can still cause deadlocks if the main thread and signal handler both try to grab the same lock, but that's not a safety issue.
Amanieu d'Antras at 2018-01-17 17:38:23
@cramertj regarding remaining blockers, I would consider the behavior that @Boiethios reported above needs to be fixed before stabilization. I filed #54901 to follow up.
David Tolnay at 2018-10-08 06:18:35
Copying @gnzlbg's comment from https://github.com/rust-lang/libc/pull/1432
It appears that 1)
thread_local!solves the problem for most people, and 2) the main uncertainty is that#[thread_local]isn't portable.Maybe we could reduce the scope of an initial version of
#[thread_local]toextern staticdeclarations. If the target does not support#[thread_local], well then theextern staticdeclaration is incorrect since there cannot be a definition anywhere, and using it would already be UB. We could add on top of this a "best effort" compilation error, e.g., if the compiler knows that the target doesn't support it.I agree with the above,
thread_local!and#[thread_local]forextern staticunblocks virtually all use-cases, and would allow interfaces toerrnousing stable Rust on all platforms (see the linkedlibcissue for more context).Right now there is no way (with stable Rust) to use C thread local variables via FFI without writing additional C glue code, see the
errno-dragonflycrate for an example of such a (necessary) hack.Joe Richey at 2019-07-15 05:28:38
cc @joshtriplett: allowing
#[thread_local]onextern statics might be something for the agenda of the WG-FFI, since interfacing witherrnois kind of an important part of the C FFI puzzle.gnzlbg at 2019-07-15 07:57:54
@gnzlbg Thanks! Agreed, we definitely need thread-local-variable support.
Josh Triplett at 2019-07-19 07:44:21
Independent of the FFI need for
extern static...Like @Amanieu from last year, I'm using
#[thread_local]in ano_stdcontext (an RTOS, in my case), with the OS runtime handling management of the TLS pointer and memory. (They and I differ on one point, which is that I'm delighted that#[thread_local]lifts theSyncrequirement forstatic. It seems good and right.)#[thread_local]is currently the only unstable feature I have to rely on for program correctness. (I'm using a couple others for convenience, but I could lower them by hand if required. I cannot easily replicate the TLS link behavior by hand.)I haven't dug into the compiler side of this, so this question may be naive, but is the
has_elf_tlsLLVM target feature not sufficient to gate this? We have a few other language features (if not attributes per se) that are gated by target support -- for example, my platform doesn't haveAtomicU64. So it doesn't seem entirely without precedent.Cliff L. Biffle at 2019-11-02 15:59:33
@alexcrichton Any news on
#[thread_local]becoming stabilized? AFAIK, at the moment it is impossible on DragonFly to accesserrnovariable from stable code, other than directly from libstd. This blocks crates likenixon DragonFly, which want to access errno as well, but libstd is not exposing it, and stable code is not allowed to use feature(thread_local).We now provide __errno_location() since this commit:
https://gitweb.dragonflybsd.org/dragonfly.git/commitdiff/60d311380ff2bf02a87700a0f3e6eb53e6034920
Antonio Huete Jimenez at 2019-12-12 10:14:54
The original issue suggests that the
thread_localattribute in LLVM isn't supported across all platforms, but has platform support improved in the time since this issue was opened? Or do we expect that it will always remain nonportable? (Is there an LLVM tracking issue?)bstrie at 2020-02-03 23:23:11
Hello,
#[thread_local]can currently be applied to fields of a struct without any errors or warnings (it won't work oc):#![feature(thread_local)] use std::sync::{Arc, Mutex}; #[derive(Debug)] struct Foo { #[thread_local] bar: &'static str, } fn main() { let foo = Arc::new(Mutex::new(Foo { bar: "bar", })); dbg!(foo.lock().unwrap().bar); let foo2 = foo.clone(); std::thread::spawn(move || { foo2.lock().unwrap().bar = "baz"; }).join().unwrap(); dbg!(foo.lock().unwrap().bar); }Outputs:
[src/main.rs:16] foo.lock().unwrap().bar = "bar" [src/main.rs:23] foo.lock().unwrap().bar = "baz"N. Nattis at 2020-08-13 16:59:09
By looking at the issue description,
#![feature(thread_local)]seems unsound, but it's not caught by theincomplete_featureslint: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=0d95cf5246b5b41ddc6776527fdc89e5Deleted user at 2020-11-20 15:33:07
Hm, that's fair... OTOH if we made
incomplete_featuresfire we'd have to enable that lint in library/std, so we'd not easily notice when another unsound feature is enabled.IMO it would be better to just make all accesses to these statics unsafe.
Ralf Jung at 2020-11-20 15:48:20
@RalfJung Well, I just read https://github.com/rust-lang/rust/pull/71435#issuecomment-618018581 after commenting and am wondering whether I should edit and hide my comment or not.
Deleted user at 2020-11-20 15:52:35
@RalfJung It seems that #49682 is the only soundness problem, so this may already have implementation strategy: banning the case in #49682. Ignore me if so. Sorry for bothering.
Deleted user at 2020-11-20 16:19:31
Well, I just read #71435 (comment) after commenting and am wondering whether I should edit and hide my comment or not.
Hm, I do not see the relation to be honest (also I was wrong when I posted that -- there is an implementation strategy).
https://github.com/rust-lang/rust/issues/54366 sounds like it might be a soundness issue if it also happens for
#[thread_local] static(withoutmut).Ralf Jung at 2020-11-20 17:13:36
@RalfJung I think I was confused by confusing comments (not only yours) and probably misread some of them. Sorry. Ignore what I said before.
Deleted user at 2020-11-20 17:35:44
Hm, that's fair... OTOH if we made
incomplete_featuresfire we'd have to enable that lint in library/std, so we'd not easily notice when another unsound feature is enabled.IMO it would be better to just make all accesses to these statics unsafe.
core,allocandstdarchhave already used#![allow(incomplete_features)]. Isstdspecial or#![allow(incomplete_features)]s in them are planned to be removed?Also, I think adding an attribute that suppresses
incomplete_featuresonly for specified features could solve the noticing issue generally (also incore,allocandstdarch).Deleted user at 2020-11-21 16:00:28
core, alloc and stdarch have already used #![allow(incomplete_features)]
I'd hope there's work happening to get rid of that. :/ I thought since the move to min_specialization, things were better. I guess this is mostly due to const-generics. At the very least there should be comments explaining why unsound features are enabled in the very foundation of Rust... but it seems not everyone agrees on sch a policy, seeing that this code was reviewed and landed. Oh well.
Also, I think adding an attribute that suppresses incomplete_features only for specified features could solve the noticing issue generally (also in core, alloc and stdarch).
That would help, yes.
Ralf Jung at 2020-11-21 16:34:33
With
relocation-model=staticcompiler uses fs "segment" to access thread local variable like expected, but withpicit uses__tls_get_addr()Shouldn't it use fs in both situation?// relocation-model=static example::f: mov byte ptr fs:[example::FOO@TPOFF+3], 1 mov qword ptr fs:[example::BAR@TPOFF], 2 ret// relocation-model=pic example::f: sub rsp, 8 lea rdi, [rip + example::FOO@TLSLD] call __tls_get_addr@PLT mov byte ptr [rax + example::FOO@DTPOFF+3], 1 mov qword ptr [rax + example::BAR@DTPOFF], 2 pop rax retSoveu at 2021-04-09 11:51:04
As far as use cases go. All the bare-metal Arm targets seem to support
#[thread_local]definitions. Not using#[thread_local]definitions leads to a lot of trouble because it appears there's no easy way, outside of compiler code, to figure out a type's memory layout in time to tell the linker what to do. It's possible to fully emulate TLS, of course, but that could be a lot of overhead for a bare-metal target.I don't have a problem, personally, sticking to Nightly for this, though.
Edit: I would worry more about these soundness issues in my project, but accessing static muts is already unsafe, so there's a built-in caveat emptor. (Project docs: "Safety: Don't let this reference escape the thread via...")
Joshua L. at 2021-05-18 19:21:19
@Soveu the POSIX/ELF AMD64 ABI has 4 models for accessing Thread-Local Storage. See this doc on Thread-Local Storage models for more information.
- Global Dynamic (Section 4.1.6)
- Uses a call to
__tls_get_addr()for each global variable access - Used for
extern/pubvariables if Rust doesn't know if the code will be statically linked
- Uses a call to
- Local Dynamic (Section 4.2.6)
- Uses a call to
__tls_get_addr()to get the current TLS offset, then uses relative offsets for subsequent accesses - Used for private variables if Rust doesn't know if the code will be statically linked
- Uses a call to
- Initial Exec (Section 4.3.6)
- Uses
fswith the GOT and a@GOTTPOFFoffset - Used for
extern/pubvariables if Rust knows if the code will be statically linked
- Uses
- Local Exec (Section 4.4.6)
- Uses
fswithout the GOT and a@TPOFFoffset - Used for private variables if Rust knows if the library code be statically linked
- Uses
Depending on how you end up linking the binary (and if you use LTO) Rust/LLVM might use one of the "Exec" models even if your relocation model is
pic. Using thestaticrelocation model just tells LLVM that the code will definitely be statically linked.This example playground should be able to show all 4 models (if you switch
pictostatic)Joe Richey at 2021-05-19 06:38:40
- Global Dynamic (Section 4.1.6)
There's also the
-Ztls-modelNightly rustc flag, if you want to force a model to be used: https://doc.rust-lang.org/unstable-book/compiler-flags/tls-model.html.Joshua L. at 2021-05-19 06:48:48
Interesting, I thought TLS is basically a thread-local copy of
.tdataandfsholds an offset between.tdataand the copySoveu at 2021-05-19 07:31:28
btw, is there a way to tell rustc to use
gsfor thread locals instead offs?Soveu at 2021-05-19 09:21:27
btw, is there a way to tell rustc to use
gsfor thread locals instead offs?I would also really like this (for Kernels and the like), but I don't think LLVM supports this (as it's not one of the 4 TLS models described above). You would have to get a 5th TLS model (maybe called
kernel) added to LLVM to support this.Joe Richey at 2021-05-19 09:43:16
I heard some rumors that Redox does that, but I can't find how
Soveu at 2021-05-19 09:59:54
I heard some rumors that Redox does that, but I can't find how
Looks like they modified their binutils: https://gitlab.redox-os.org/redox-os/binutils-gdb/-/merge_requests/5
Joe Richey at 2021-05-19 10:15:58
I heard some rumors that Redox does that, but I can't find how
Looks like they modified their binutils: https://gitlab.redox-os.org/redox-os/binutils-gdb/-/merge_requests/5
Would have been better if they made this pull request to actual binutils then we all could use it.
Anhad Singh at 2021-05-20 07:40:48
Okay, I think I found a way to make things work using just
#[thread_local]declarations, instead of full definitions. Only requires some form ofasmsupport. playgroundThe trick is figuring out how to link the extern declaration to a full definition without LLVM noticing and ignoring the original declaration. The
.equivasm directive seems to accomplish this with minimal voodoo magic.Joshua L. at 2021-05-24 15:55:35
Would it be possible to partially stabilize this for let's say x86_64 architecture in user space code and by also not supporting problematic types like generators etc?
Élie ROUDNINSKI at 2021-06-15 14:38:37
Marking as impl-incomplete since it may still have soundness issues, and may misbehave on platforms without TLS support.
Josh Triplett at 2021-11-10 18:53:50
EDIT: original proposal here was unsound wrt scoped threads, see https://github.com/rust-lang/rust/issues/29594#issuecomment-1125466642
<details> <summary>(click to open the original proposal)</summary>I'm not sure where this should go, and I don't have the time for a complete RFC, but "a lifetime shorter than
<hr/>'static" came up recently in the context of somerustcinternal data structures (which are owned thread-locally, not coincidentally), so:(with apologies to any previous suggestions similar to this, that may have been dismissed in the past - I couldn't find anything in this very issue, at least)
If we added a
'threadlifetime, like'staticbut strictly shorter:- borrowing a
#[thread_local](and eventhread_local!) could produce a&'thread Treference- it should even be returnable from functions (as long as it doesn't leave the thread, see below)
- to preserve lifetime parametericity,
&'thread T: Send + Synchas to be true ifT: Sync(just like with&'a Tfor any other'a), and soundness of'threadusage shouldn't require it- in fact, it should be entirely possible to pass such a
&'thread T"down" to a scoped thread, just like howf(&THREAD_LOCAL)(with#[thread_local]) orTHREAD_LOCAL.with(|data| f(data))(withthread_local!) work today
- in fact, it should be entirely possible to pass such a
- everything that requires
T: 'statictoday would still not allow anything containing these new'threadlifetimes, and that includes:- potentially-detachable (i.e. non-scoped) threads
- any kind of communication between non-scoped threads is also indirectly affected by that bound, i.e.
mpsc::Receiver<T>: 'staticrequiresT: 'static - this also extends to thread pools running
asynctasks: aFuturefromasync fn, that is required to be'static, cannot keep a&'thread Tacross a suspension point
- any kind of communication between non-scoped threads is also indirectly affected by that bound, i.e.
thread_local!'s data type- essentially stating "no
&'thread Tcan get from the normal thread execution, intothread_local!destructor execution" - a
thread_local!destructor could itself obtain a&'thread Treference, but it would be limited in scope to that destructor's execution, since there would be no place available to stash it - for this to be sound, I believe
#[thread_local]also needs the'staticbound, otherwise it could hold a reference to athread_local!, that a differentthread_local!'s destructor could read back
- essentially stating "no
- potentially-detachable (i.e. non-scoped) threads
- there are two styles for "getting short-lived references" APIs today:
fn with(&self, f: impl FnOnce(&T))- only option sound today for thread-local
Ts,thread_local!uses it
- only option sound today for thread-local
fn get(&self) -> &T(Deref-like)- with owned
self, this is unsound becauseBox::leak(Box::new(self)).get()returns a&'static T, even ifselfitself is!Send/!Sync - this is (incorrectly) used in a few places in
rustc, today, and that's where this whole idea came from
- with owned
- but with
'thread, we could have the best of both worlds, and return&'thread T, to be more flexible than today's sound option (with) while (hopefully?) remaining sound- only downside is
Derefcan't be implemented directly (except on a type that has&'thread Tas a field itself)
- only downside is
- a
structwithPhantomData<&'thread ()>would make that type never pass a'staticbound check, so evenBox::leak(Box::new(self))would only produce a&'thread Self, not a&'static Self, meaning aDeref-like API would actually remain sound- though this sort of thing could complicate the implementation, unless it's only allowed by making the
structlifetime-generic and passing'threadin that way
- though this sort of thing could complicate the implementation, unless it's only allowed by making the
Based on the little I remember about how
'staticis implemented, there's a good chance of this being easy to fully implement (copy how'staticis handled, but make it strictly "shorter" in the lattice), but I wouldn't bet on it just yet.EDIT:
- while looking around for precedents, I found a full-fledged RFC (https://github.com/rust-lang/rfcs/pull/1705) from 2016, that I had completely forgot about - looking in it now to compare it...
- oh, it made the classic mistake of proposing
&'thread T: !Send(which is impossible because of lifetime parametericity, see above) instead of relying only on'thread < 'static:Any type depending on
'thread(i.e., a type product of the type construction from'thread) is!Send, and thus bounded to the current thread. - @nikomatsakis caught onto that issue in this comment: https://github.com/rust-lang/rfcs/pull/1705#issuecomment-241499891
- overall I feel like that RFC was not arguing for itself very well - the
fn-scoped#[thread_local]limitation we ended up with instead is enough for soundness, and there is no talk about letting theLocalKeyAPI ofthread_local!use the'threadlifetime etc.
Eduard-Mihai Burtescu at 2021-12-21 17:27:14
- borrowing a
@eddyb I really hope for this feature to be stabilized as soon as possible, but I must ask the following question:
it should be entirely possible to pass such a &'thread T "down" to a scoped thread
Is it guaranteed that the same thread-local pointer stays valid when passed to a different thread? I can imagine a system which maps the same numeric pointer to different physical addresses for different threads via MMU magic.
Artyom Pavlov at 2021-12-22 19:22:05
Is it guaranteed that the same thread-local pointer stays valid when passed to a different thread? I can imagine a system which has the same numeric pointer to point to different physical addresses for different threads via MMU magic.
That is not allowed. Thread-local variables for different threads must have distinct addresses.
Amanieu d'Antras at 2021-12-22 19:26:07
Is it guaranteed that the same thread-local pointer stays valid when passed to a different thread? I can imagine a system which has the same numeric pointer to point to different physical addresses for different threads via MMU magic.
That is not allowed. Thread-local variables for different threads must have distinct addresses.
@newpavlov To expand a bit further: such a pointer could not, in Rust, use the type
&T, and instead would have to be some kind of wrapper that only produces a&Tif it can compute a "global" pointer (i.e. one valid across all threads).If
T: Sync, then&T: Sendholds and the pointer can make its way into a different thread, as long as it's not accessed outside of its original scope. Even with a!Syncpointee, I'd still be wary of any further derived reference existing (&Foodoesn't giveFooas much control over the reference as a separateFooReftype would).This applies to
thread_local!'s.with(|short_lived_ref| ...)method today, which doesn't stop you in any way from e.g. spawning some scoped threads and capturingshort_lived_refin them, inside the closure. (And#[thread_local] statics have their equivalent, wheredo_anything_with(&THREAD_LOCAL_STATIC)passes the reference "down the stack" to a function that doesn't really see it as any other reference).Eduard-Mihai Burtescu at 2021-12-23 00:05:31
Reading back my sketch for for
'threadabove (https://github.com/rust-lang/rust/issues/29594#issuecomment-998963161), and specifically:in fact, it should be entirely possible to pass such a
&'thread T"down" to a scoped thread,I had some thoughts about interesting lattice interpretations of
'thread, which is that it's somewhere between'staticand all other stack-related lifetimes within a thread.With detached threads requiring
'staticbounds to pass data between them, each detached thread ends up with a hierarchy of'static > 'threadX > ...(for some thread X), so'threadcan be seen as a kind of combination (union/intersection aka join/meet, whichever is correct) of all'threadX.But that only works for detached threads - for scoped threads you end up with
'threadXlifetimes that can be arbitrarily small, which means'threadactually becomes isomorphic "the empty lifetime" (i.e. it has to be treated as shorter than any other lifetime, kind of like a "bottom" equivalent) and it's impossible to do almost anything with it, if you're to remain sound.In fewer words, my original proposal was unsound as stated.
As an example, imagine
&'a Cell<&'thread T>to a scoped thread - if it can place its own TLS references in there, those will become invalid when the scoped thread exits. But if we take the correct interpretation mentioned earlier, that type is illegal, because'threadis shorter than any lifetime (including'a) - problem averted, right?Well but now you can never have
<hr/>let x = &THREAD_LOCAL;becausexhas a scope that's arguably longer than'thread(it's shorter than the current thread, but with just one'threadthere's no way to distinguish). So you're either useful and unsound wrt scoped threads, or sound but useless.There might be a way to salvage the hope of a
'threadthat doesn't need to interfere withSync/Sendat all, which I didn't come up with myself but was suggested to me by @eternaleye:'threadcould become an implicit extra lifetime parameter to all functions. Given @tmandry and @nikomatsakis' discussions around varieties of contextual implicits, it could be seen as a compiler-impliedwith 'thread.To limit compilation performance impact and whatnot, it would ideally only be nameable anywhere in the function if it shows up in the signature or
whereclauses, and calling a function that needs'threadfrom one that doesn't, could just use the outermost scope of the caller (effectively "statically known top of the thread", which is appropriate, given that the caller could literally be a thread entry-point for all we know).For detached threads, we have the same solution:
'threadwon't pass the'staticbounds required by e.g.thread::spawn, unless you have a'thread: 'staticbound on the caller - which could either never be satisfiable, or, as an interesting twist, could perhaps be satisfiable insidefn mainspecifically, encoding in the language that "the main thread lives forever" (and yes this would also mean&THREAD_LOCALfrom withinfn mainwould be&'static- could have interesting implications).For scoped threads, it boils down to "only direct calls to functions pass
'thread" - the amount of dynamism (whetherfnpointers ordyn FnOnce) involved forces the new thread's entry-point to effectively be'thread-polymorphic, and anything using'threadfrom the parent thread looks like it could be any stack lifetime.The earlier example of
&'a Cell<&'thread T>would just be a&'a Cell<&'b T>, and&'b Tcould only be created by the scoped thread using any other&'breferences it may have gotten from its parent thread.In fact, without some
with 'thread-like abstraction at the traitimpllevel, even calling a trait method should probably leave the callee'thread-polymorphic for now. So writing'threadanywhere other than in the signature/whereclauses of a free function or inherent impl, should be an error (i.e. a type or trait definition should take an explicit lifetime parameter and not refer to'thread).A limited version of this feature could be implemented right away, and because it's mostly just desugaring to lifetime parameters (with only the choice of what's passed for the parameter in direct calls, and the lifetime in
&THREAD_LOCAL's type, being'thread-specific semantics), it's way more likely to be sound.Eduard-Mihai Burtescu at 2022-05-12 22:11:51
#[thread_local]causes an Internal Compiler Error if used with proc macro-specific types (likeproc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}). Could we either- document that it's not for proc macro-specific types, or
- have a tracking issue for this, please?
Peter Lyons Kehl at 2023-04-24 05:51:00
https://github.com/rust-lang/rust/issues/115621
segment fault on windows. is it a misuse of the feature, or a bug?
cybersoulK at 2023-09-06 23:13:12
Would it makes sense to just reject the attribute on targets where
target_thread_localis not set?Ralf Jung at 2023-09-07 09:57:29
#[thread_local] causes an Internal Compiler Error if used with proc macro-specific types (like proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}). Could we either
This has nothing to do with this tracking issue; it's about the entire proc-macro system being incompatible with thread-local state (no matter how that state got implemented).
This tracking issue is about thread-local state specifically implemented via the native mechanism of the linker (as opposed to something like pthread keys). The
thread_local!macro has different implementations depending on the target; sometimes it uses linker-native thread-locals (internally using the feature tracked here), sometimes it uses slower but less fragile run-time OS-provided mechanisms.Ralf Jung at 2023-09-07 09:59:00
I wonder if there's a path towards a minimal
thread_localstabilization by being very restrictive?E.g.:
- all access is by value only
- only allow
Copytypes (or maybe even restrict it to only a subset of primitives) - don't allow lifetimes
- don't allow composing
thread_localwith other lang attributes (they may interact in "interesting" ways) - make
thread_localerror if the platform does not support it. This would imply also stabilizingtarget_thread_localbut maybe the name should be subject to bikeshedding first (e.g.has_static_thread_local) to be clear that it may still have runtime thread locals.
Chris Denton at 2023-12-22 16:25:26
What is the motivation for that? Doesn't
thread_local! { static NAME = const { ... } }suffice?Ralf Jung at 2023-12-22 16:28:32
no_stdmainlyChris Denton at 2023-12-22 16:29:33
Does that form of the macro need anything that requires
std? Could there be acore::thread_local! { ... }macro that makes that part of the functionality available withoutstd(i.e., it would requireconstblocks)?Ralf Jung at 2023-12-22 16:33:01
We could make a macro I guess. It'd be pretty redundant though when/if
thread_localproper is stabilized. Unless we decide to just have a macro and not the attribute.Chris Denton at 2023-12-22 16:34:51
The API provided by
#[thread_local] staticand const-thread_local is pretty different I think. So your proposal does introduce redundancy that we currently don't have. There'd be two stable ways to do the same thing and two completely different mechanisms to ensure the necessary restrictions (such as "no'staticreferences").Ralf Jung at 2023-12-22 16:36:36
The "two ways to do things" will occur whatever happens unless we simply don't stabilize
#[thread_local] static.Chris Denton at 2023-12-22 16:39:05
That is true. If the usecases are covered I don't see a reason to stabilize
#[thread_local] static. It could be transitioned to an internal feature, an implementation detail of the public macros.Ralf Jung at 2023-12-22 16:40:53
core::thread_local!would also needLocalKeymoved intocore::thread(or an equivalent to it). One thing thatthread_local!forces that#[thread_local]doesn't is going through a shared reference. There would also still need to be a way to know ifthread_localis supported inno_std.Chris Denton at 2023-12-22 17:39:07
What is the motivation for that? Doesn't thread_local! { static NAME = const { ... } } suffice?
IIRC generated assembly for
thread_local!was quite ugly when compared to equivalent#[thread_local]code. I haven't measured performance impact and do not know if it's possible to work around it withstdchanges, but it's still an example of non-zero-costness. Also,#[thread_local]-based code simply looks nicer and more ergonomic.Artyom Pavlov at 2023-12-22 18:52:18
For the new
thread_local! { static FOO: ... = const { ... }syntax it should all be optimized down to a minimum. If not then we should really try to fix that.EDIT: assuming the type of
FOOhas no drop, of course.Chris Denton at 2023-12-22 18:59:47
Also, #[thread_local]-based code simply looks nicer and more ergonomic.
If they only work for
Copytypes and you can't take any references (and hence also not call any&self/&mut selfmethods), I am not sure if that's still true.And taking references would be unsound.
Ralf Jung at 2023-12-22 19:17:05
And taking references would be unsound.
Borrowck limits those references to the current function already I believe.
bjorn3 at 2023-12-22 19:23:57
Yes. The goal would be to eventually make full
#[thread_local]stabilized (once bugs, etc are fixed). But that's not happening any time soon and in the meantime a minimal stabilization would be useful.Chris Denton at 2023-12-22 19:26:07
Oh interesting, I wasn't aware that borrowck had special treatment for thread_local statics.
Ralf Jung at 2023-12-22 21:31:36
I'd be very interested in seeing this stablized. I use
#[thread_local]in the interface code for my WIP OS, as an import/export for thread_local statics (https://github.com/LiliumOS/lilium-sys/blob/main/src/sys/io.rs#L44-L54). The handles here are thread-local (as handles themselves are thread-local resources in the OS) that are initialized by the USI (userspace standard interface) when you create a thread (This specific case is limited to aCopytype and you probably won't be be borrowing the handles often at all).In general, being able to import an external TLS var without shim code written in C is useful (for example, if you want to grab
__errno). This cannot be done withLocalKeyandstd::thread_local!.#[thread_local]would also enable using TLS in a no-std context, such as a kernel.Connor Horman at 2023-12-22 22:34:49
@RalfJung
If they only work for Copy types and you can't take any references (and hence also not call any &self/&mut self methods), I am not sure if that's still true.
How about introducing a pointer-based variant of
#[thread_local]? Something like this:// Creates TLS value which stores 42 and creates "pointer" `FOO` to it. #[thread_local_ptr] static FOO: *mut u64 = 42u64; fn increment_foo() { // SAFETY: reference to `FOO`does not escape execution thread let foo: &mut u64 = unsafe { &mut *FOO }; *foo += 1; }Artyom Pavlov at 2023-12-23 14:10:51
@newpavlov it seems someone already taught the borrow checker about thread_local so taking references to them is actually sound. That's pretty cool.
I don't like there being this API duplication and inconsistency between that and the
thread_local!macro, but that's up to libs-api to figure out.We'd have to be rather careful where we enable this feature; in the past, I think on some Windows targets we switched back-and-forth between "true" thread-local statics and some other implementation for the
thread_local!macro. The macro gives us the flexibility to do that;#[thread_local]does not, so once we allow it somewhere, if we later figure out there is some platform issue then we are in trouble.I'm slightly bothered by thread-local statics pretending to be statics. They have very little in common with regular
staticin terms of their semantics.&FOOisn't even a constant, it is a function. We do reflect this in the MIR at least so bugs due to this are hopefully unlikely. And I don't have a proposal for a better syntax either so :shrug:Ralf Jung at 2023-12-23 16:01:46
I have a few use cases for
#[thread_local]that can't be satisfied by the standard library'sthread_local!macro:- The code base is a
no_stdbinary which doesn't depend on libc (it has its own TLS/stack initialization code). - It exports
#[thread_local]variables for use by C code (errno). - It used to
#[thread_local]variables in inline assembly (by symbol name), although that code has since been refactored and it not longer does that. It is possible to know the exact instruction sequence to use with the symbol because the whole code base is compiled with-Z tls-model=local-exec.
Amanieu d'Antras at 2024-01-02 19:44:28
- The code base is a
#[thread_local]translates directly to thethread_localattribute in LLVM. This isn't supported on all platforms, and it's not even supported on all distributions within the same platform (e.g. 10.6 doesn't support it but 10.7 does). I don't think this is necessarily a blocker, but I also don't think we have many attributes and such which are so platform specific like this.What's the status of this? I see a lot of discussion around a hypothetical
cfg(target_thread_local), but nothing concrete?Claudia Meadows at 2024-08-15 05:34:41
It's not hypothetical,
cfg(target_thread_local)exists on nightly. However, historically we have turned this on and off on some platforms to work around various bugs, so we should be careful before just blanket-exposing this on stable.It used to #[thread_local] variables in inline assembly (by symbol name), although that code has since been refactored and it not longer does that. It is possible to know the exact instruction sequence to use with the symbol because the whole code base is compiled with -Z tls-model=local-exec.
If we just allow asm blocks to reference thread-locals, I worry it will lead to much confusion and errors. A thread-local is not just a normal symbol, after all -- but a Rust programmer might think it is, since in Rust code it behaves much like a static, but that's a sweet lie.
Unfortunately the inline asm docs don't even give an example for how
symis used at all. They do mention this though:<path> is allowed to point to a #[thread_local] static, in which case the asm code can combine the symbol with relocations (e.g. @plt, @TPOFF) to read from thread-local data.
Presumably, it is UB to try to access that symbol without the exactly right set of relocations matching the current target and build flags? Seems like a pretty big footgun.
Ralf Jung at 2024-08-15 05:58:38
Presumably, it is UB to try to access that symbol without the exactly right set of relocations matching the current target and build flags? Seems like a pretty big footgun.
It's always safe to use the most general relocations (which involve calling
__tls_get_addr). It's the more specific ones like@tpoffwhich are only valid with certain TLS models (for examplelocal-execis only valid in executables, not shared libraries).Amanieu d'Antras at 2024-08-16 16:42:58
Using
__tls_get_addrrequires you to emit a very specific set of bytes and relocations (on x86 this includes redundant prefixes). Getting anything wrong will likely cause tls relaxation by the linker to either error or generate a corrupt binary. And even with the most general relocations you still have to deal with different object file formats using different ways of accessing TLS. We currently don't have any cfg's for the object file format, so doing something that works correct on any OS for a given architecture is not possible.bjorn3 at 2024-08-16 16:53:10
Just curious, are there any thread-local modes that aren't based on ELF's thread structure system? e.g. on x86_64 will we eventually be able to simply emit instructions that use FS/GS segments directly instead of reading a pointer from a negative offset and de-referencing it?
As far as I can tell that's GNU's/ELF's TLS model that doesn't really translate nicely to
#![no_std]code (sans-OS) on x86_64.Josh Junon at 2024-08-17 08:55:05