Implement Thread::os_id - #160219
Conversation
| use thread_name_string::ThreadNameString; | ||
|
|
||
| // The handle of a spawned thread exists before the thread does, so the thread | ||
| // stores its own id once it starts running, hence the atomic. 0 means "not known". |
There was a problem hiding this comment.
Nothing guarantees that the OS's ID is non-zero, we really shouldn't use zero as a sentinel.
There was a problem hiding this comment.
Switched to OnceLock: write-once without a sentinel, and it needs no 64-bit atomic so the cfg_select is gone too.
Waiting would need the child to always store the value, so OnceLock<Option>.
I'm also not sure how it works out for spawn hooks, which get &Thread on the parent before the thread exists.
You mentioned you have an implementation, so if you've already worked that out I'd rather build on it than guess.
|
Reminder, once the PR becomes ready for a review, use |
|
@rustbot ready |
|
A meta comment: I appreciate your recent contributions to the project, you are clearly interested in helping out and invest time and thought into it. At the same time, especially the description of this PR makes it very obvious that you are using a LLM to aid you in your contributions, both in writing code and comments. These kinds of cases have been very intensely discussed within the project, and the result of that discussion is our LLM policy that will come into effect next week. Under that policy your LLM usage is deemed forbidden, and failing to declare it (like you have been doing) may result in moderation actions against you. I'm not a moderator, and this is not a moderation warning, just some friendly advice that I want to give you, stemming from years of working on the standard library: In my experience, the hard part about working on the standard library is not actually writing the code, but in
An LLM can do neither those things. While they are admittedly good at doing the mechanical task of writing the code, they are awfully unreliable when it comes to providing evidence, have little to no intrinsic capability to exercise good judgement and are fundamentally just not you. As an example, the current description of this PR is mostly just a very detailed summary of its changes. I can see those myself, thank you very much, that's what the "Files changed" tab is for! The much more interesting questions in this case are e.g. why Thus, please, remember to take time to think and research and be the author, not just the editor, of your communication and your code. If you want to learn how best to use (or not use) LLMs, feel free to join our LLM-mentoring channel on Zulip. As for me, I'm not interested in some stochastic parrot's output, but in other people's, since that's what I learn and thrive from. That's why I invest my time in this project anyway, and am very happy to help other people out if they get stuck. I must insist however that the responsibility of doing the thinking and research doesn't fall on my shoulders alone. @rustbot author |
Thanks for your advice, I totally agree with inappropriate use of LLM in this PR, I usually trying to take more time preparing the PR and understanding all the nuances, this one was bad and too heavily relied on AI. Thanks and I will take it into consideration. As for this PR, I will take some time to really reason about it and also consider other approaches. Sorry for that. :) |
| /// use std::thread; | ||
| /// | ||
| /// let spawned = thread::spawn(|| thread::current().os_id()); | ||
| /// println!("spawned thread ran as {:?}", spawned.join().unwrap()); |
There was a problem hiding this comment.
Could this do the assert_ne! test here? I think that demos relevant properties a bit better.
There was a problem hiding this comment.
Thanks, applied the changes but guarded it so it will not fail on other platforms that do not support os_id and could return None making it fail.
| /// The operating system may hand the same id to a later thread once this one | ||
| /// exits, so it does not name a thread uniquely over the life of the | ||
| /// process. It may also no longer refer to this thread at all, since any | ||
| /// thread but the current one can exit at any point. For anything other than | ||
| /// the current thread, logging is the only safe use. |
There was a problem hiding this comment.
It's usable for any thread that is running, not just the current right?
I think the property to convey is that OS TIDs uniquely represent a thread among other running threads, which effectively means that if a thread isn't known to be running then ID can only be used in cases where non-uniqueness is okay (e.g. logging). And then one way to know the thread is running is if you're looking at the current thread's ID.
Not sure how best to put this into words.
There was a problem hiding this comment.
So there are conditions under which it is safe to use os_id and it refers to correct thread, however in other conditions it should be used only for cases when stale os_id reference is harmless, like logging.
I have tried to reword this section to better communicate this, thank. Let me know if you see any better way to put this into words.
| /// | ||
| /// This is the id that shows up in tools like `ps` and `top`, debuggers and | ||
| /// crash logs, unlike [`ThreadId`], which has no guaranteed relationship to | ||
| /// it. `None` means the platform has no such id or offers no way to read it, |
There was a problem hiding this comment.
Some sandboxes or containers allow per-function filtering, so it is also possible the thread ID exists with an API to read it, but the process can't do that.
| /// it. `None` means the platform has no such id or offers no way to read it, | |
| /// it. `None` means the platform has no such id or it can't be read, |
There was a problem hiding this comment.
Thanks, applied your suggestion to reword it slightly.
| let thread = Thread::new(id::get_or_init(), None); | ||
| thread.set_os_id_to_current(); |
There was a problem hiding this comment.
Why doesn't Thread::new just call set_os_id_to_current internally?
If there are callers where this doesn't work, maybe we should have two constructors?
Thread::new_currentuses current OS idThread::new_remotetakes OS id as paramter
I think this would also avoid the OnceLock.
There was a problem hiding this comment.
I added Thread::new_current just for the current path, however I think we couldn't get rid of sync primitives for os_id because of the spawn_unchecked path, in that case when we create the Thread, we are still running on parent thread, so using set_os_id_to_current would assign parent os_id to child thread that it is trying to spawn.
- Thread::new lifecycle.rs:48 (imp::current_os_id() returns the parent's TID)
- imp::Thread::new lifecycle.rs:116
|
@rustbot ready I have addressed suggested refinements. PR body was updated to better explain decisions for the implementation and further possible refinements, it is pretty long and detailed but I think those nuances worth explaining properly. |
|
Sorry I'm at capacity for the next two weeks. @rustbot reroll |
Implements
Thread::os_idas an unstable feature, per the accepted ACP rust-lang/libs-team#635.Tracking issue: #160215
os_idreturns the OS-level thread id, so Rust programs can tie their own logs to system-level logs (the ACP's stated motivation).It's a snapshot taken when the thread starts running: the OS can reuse a TID after the thread that held it exits, so it isn't guaranteed to be unique.
The std
Threadcan outlive the OS-level thread, soos_idcan go stale. It has no guaranteed relation toThreadId.Implementation
Based on the decision in the ACP,
Threaddoes not store apthread_t; the two have separate lifecycles.pthread_tgoes invalid on join or detach,Threadis anArcand outlives that.Not storing a
pthread_tinside the stdThreadis why it can't be queried on demand. Even when apthread_texists (on the native handle), some platforms have no by-handle query at all, or it isn't plumbed through Rust'slibc. (details below)That's why
os_iddoes not represent direct, up-to-date information but rather a snapshot of theos_idwhen the thread starts.There are 2 categories of handle creation (3 call sites):
t0. Thread::new lifecycle.rs:48 (imp::current_os_id() returns the parent's TID) - this is why we can't set os_id inside Thread::new, it will assign parent TID to child Thread.
run_spawn_hooks(&thread) lifecycle.rs:53 - user code already holds &Thread here, but no related OS thread exists yet. As long as hooks are handed a &Thread before creation, no design can close that window.
t1. imp::Thread::new lifecycle.rs:116 - (imp::current_os_id() still returns the parent's TID during the call), the only way to get os_id here, once it returns, is by querying handle, not current_os_id.
t2. thread_start runs ThreadInit::init lifecycle.rs:146, using imp::current_os_id() and setting the thread's os_id.
The earliest time we could get os_id is t1, by querying the handle. (Current PR handles t2 case)
Only the spawn path writes late, and by then the handle is already shared: the parent can call (
JoinHandle::thread().os_id()beforejoin(), for example). So the field needs synchronization.The field is set after the handle already exists, once the thread is running; if it hasn't started yet,
os_idgivesNone.The signature matches the ACP's
fn os_id(&self) -> Option<u64>, but the reason is new - the ACP's None meant "platform doesn't support it", not-yet-started is an extra case represented by None state for os_id.The following options were considered for the
Thread::os_idsynchronization primitive:Atomic<u64>and 0 sentinel, nothing guarantees OS tid is never 0, also for targets without 64-bit atomics platform specific handling will be needed (cfg_select!) -OnceLockneeds none of that.OnceLock::waitwould change an accessor into a blocking call: ifimp::Thread::newreturnsErr,os_idis never set, andwait()would hang forever - that's why it isn't used here.Further improvements
This PR is a starting point, there is room for further improvements and refinements for
os_idin follow up PRs.In spawn path os_id is filled by the child itself when it starts, further improvements could aim for setting os_id by parent right after child thread is created, by querying the handle to get os_id from pthread, platform specific interfaces exist for some platforms. Making current approach (child itself set os_id on start) as fallback for cases when platform doesn't support querying by handle.
It could introduce the race. A parent-side fill after creating a thread and the child's own write at start (
self.handle.set_os_id_to_current()) would both write the same field, concurrently.OnceLockalready absorbs that today - thread.rs:132,let _ = self.inner.os_id.set(os_id)- onesetwins, the other'sErris discarded.That's why
OnceLockstays under this design rather than being replaced by anything else.Per platform:
libccrate?pthread_threadid_nppthread_tis the tidGetThreadIdpthread_gettid_nppthread_gettid_np, glibc 2.42+Sketch, per platform:
And in
spawn_unchecked, right after creation succeeds:Cost. A new
imp::Thread::os_id()needs an arm in each platform that support it.Testing
On aarch64-apple-darwin:
./x check library/std./x test library/std --no-doc --test-args thread_os_id, 3 passed (the two new ones and the existingtest_thread_os_id_not_equal)./x test library/std --doc --test-args os_id, 1 passed./x fmt --check --allreports no changesr? libs