Skip to content

Implement Thread::os_id - #160219

Open
valentynkit wants to merge 6 commits into
rust-lang:mainfrom
valentynkit:thread-os-id
Open

Implement Thread::os_id#160219
valentynkit wants to merge 6 commits into
rust-lang:mainfrom
valentynkit:thread-os-id

Conversation

@valentynkit

@valentynkit valentynkit commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

View all comments

Implements Thread::os_id as an unstable feature, per the accepted ACP rust-lang/libs-team#635.

Tracking issue: #160215

os_id returns the OS-level thread id, so Rust programs can tie their own logs to system-level logs (the ACP's stated motivation).

  • Using the existing current_os_id is much simpler than pulling the id off imp::Thread in spawn_unchecked. That needs a per-platform arm, and the child still has to fill it in on platforms without a by handle query, so it'd be extra on top of this rather than instead of it.
  • Thread is handed to user code by spawn hooks before the native thread exists, so the id can only be filled in later. There's no spare u64 value to mean "not set yet", so a OnceLock is chosen as a simple primitive to use for this purpose.

r? libs

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 30, 2026
Comment thread library/std/src/thread/thread.rs Outdated
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".

@joboet joboet Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing guarantees that the OS's ID is non-zero, we really shouldn't use zero as a sentinel.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 30, 2026
@rustbot

rustbot commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

@valentynkit

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 30, 2026
@joboet

joboet commented Jul 30, 2026

Copy link
Copy Markdown
Member

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

  1. Doing the research and taking the thinking time to verify that the changes are truly correct. The standard library ships on literally billions of devices, we must try very hard to ensure that it works as intended.
  2. Communicating that work to other people so that not just you, but the person reviewing your work (in this case, me) and everyone else can follow along.

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 OnceLock is correct in this case (both you and me need to think this through in any case, the LLM cannot lift that responsibility off our shoulders), or how one might do better (here you'd need e.g. links to platform documentation – which is rather difficult for an LLM to find especially in the curious case of pthread_gettid_np), or why this PR is still a good point to start from (that's a matter of taste and opinion, I see why one might argue that it is). And if you've thought about all these questions, then writing these kinds of PRs entirely on your own is actually easier than proofreading the LLMs output and explaining all your thoughts to it.

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
until you update the PR description with something entirely of your own.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jul 30, 2026
@valentynkit

Copy link
Copy Markdown
Contributor Author

In my experience, the hard part about working on the standard library is not actually writing the code, but in

1. Doing the research and taking the thinking time to verify that the changes are truly correct. The standard library ships on literally _billions_ of devices, we must try very hard to ensure that it works as intended.

2. Communicating that work to other people so that not just you, but the person reviewing your work (in this case, me) and everyone else can follow along.

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.
Especially about using OnceLock or alternative approaches.

Sorry for that. :)

Comment thread library/std/src/thread/thread.rs Outdated
/// use std::thread;
///
/// let spawned = thread::spawn(|| thread::current().os_id());
/// println!("spawned thread ran as {:?}", spawned.join().unwrap());

@tgross35 tgross35 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this do the assert_ne! test here? I think that demos relevant properties a bit better.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread library/std/src/thread/thread.rs Outdated
Comment on lines +229 to +233
/// 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.

@tgross35 tgross35 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread library/std/src/thread/thread.rs Outdated
///
/// 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,

@teor2345 teor2345 Jul 31, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
/// 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,

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, applied your suggestion to reword it slightly.

Comment thread library/std/src/thread/current.rs Outdated
Comment on lines +249 to +250
let thread = Thread::new(id::get_or_init(), None);
thread.set_os_id_to_current();

@Darksonn Darksonn Jul 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_current uses current OS id
  • Thread::new_remote takes OS id as paramter

I think this would also avoid the OnceLock.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. Thread::new lifecycle.rs:48 (imp::current_os_id() returns the parent's TID)
  2. imp::Thread::new lifecycle.rs:116

@valentynkit

Copy link
Copy Markdown
Contributor Author

@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.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Aug 2, 2026
@Darksonn

Darksonn commented Aug 5, 2026

Copy link
Copy Markdown
Member

Sorry I'm at capacity for the next two weeks.

@rustbot reroll

@rustbot rustbot assigned Mark-Simulacrum and unassigned Darksonn Aug 5, 2026

@Mark-Simulacrum Mark-Simulacrum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the PR description should be significantly cut down before we merge this to simplify the git history (and help PR reviewers).

I would suggest something like these two bullet points:

  • It's much simpler to use the existing APIs that retrieve via gettid-equivalents rather than trying to extract it out of the imp::Thread in spawn_unchecked.
  • I don't think you need to spend more time on this in the PR description. The analysis of future possibilities can be filed as a separate issue, though I'm not convinced the benefit of an avoided syscall is really justified to merit the extra complexity.
  • Thread is exposed to user code via spawn hooks before the native thread exists, so the native ID must be lazily initialized. Since the value has no obvious niches, a OnceLock is chosen as a simple primitive to use for this purpose.

View changes since this review

Comment thread library/std/src/thread/thread.rs Outdated
/// Creates a handle for the calling thread, recording its OS id.
///
/// `id` must be the `ThreadId` of the calling thread.
pub(crate) fn new_current(id: ThreadId, name: Option<String>) -> Thread {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the first part of #160776, I think it is never valid to pass a name into this function since that can cause usage of the Global allocator. Looking at the impl here I think we always pass None, so it seems reasonable to delete the name argument entirely (and leave a comment)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, added a comment:

/// Takes no name because passing one into Thread::new allocates with the
/// global allocator, which thread::current is documented never to use.

Comment thread library/std/src/thread/thread.rs Outdated
Comment on lines +238 to +243
/// Ids are unique among threads running at the same moment, but the
/// operating system may reuse the id of a thread that has exited, and a
/// `Thread` handle can outlive the thread it refers to. As long as you know
/// the thread is running, the id still refers to that thread; for the
/// current thread you always know. When you do not, use the id only where a
/// repeated id is harmless, such as logging.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have an example of why it would be useful to rely on the (weak) uniqueness guarantee given here? It seems simpler to me to just say that this is the thread id provided by the OS (if one was captured), but make no further guarantees about it. std::process:id just gives that definition with no uniqueness guarantees.

I think at least Linux's pid namespaces guarantee (today) that threads in the same program can't overlap OS IDs, but if we do want to say something about uniqueness it seems worth noting that cross-process that guarantee may not be true.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual pages (or POSIX) only guarantees that thread IDs are unique within a process at a single point in time:
https://www.man7.org/linux/man-pages/man3/pthread_self.3.html#NOTES

Note also that PID namespaces exist, so it is possible to have duplicate PIDs (and I assume thread IDs as well):
https://www.man7.org/linux/man-pages/man7/pid_namespaces.7.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cut these (weak) uniqueness guarantee, it doesn't seem worth stating, nor the platform-specific case.
Let me know know if you would rather have it, and document the precise per-platform guarantees.

/// spawned thread does this itself once it starts running, since its handle
/// already exists by then.
pub(crate) fn set_os_id_to_current(&self) {
if let Some(os_id) = imp::current_os_id() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SGX impl of current_os_id appears to return the address of thread::current()'s allocated Arc<Thread> if I'm reading it right. I think under the current design, that allocation is not guaranteed to exist and so this will hit the BUSY / re-entrant case in thread::current?

Specifically the sequence is:

  • Foreign spawn -- e.g. via pthread, not spawn_unchecked
  • Thread runs and calls thread::current()
  • Calls Thread::new_current
  • Calls imp::current_os_id
  • Calls thread::current()

(On the spawn_unchecked path we'd set_current before we hit this code).

I think the two fixes are either (a) we modify thread::current() to call set_os_id after initializing the thread-local pointer to Arc or (b) we change SGX to have some other implementation (e.g. use the Rust ID).

cc @jethrogb @raoulstrackx @aditijannu (sgx target maintainers), in case you have an opinion on the "OS" IDs of threads for the target (https://doc.rust-lang.org/nightly/rustc/platform-support/x86_64-fortanix-unknown-sgx.html).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That thread::current() in SGX isn't std::thread::current
sgx.rs imports thread from crate::sys::pal::abi, so it resolves to abi/thread.rs. So I don't see the path that will hit the BUSY case in std::thread::current.

I went through every current_os_id impl and none calls std::thread::current, it may only be true for today, so I've documented it on set_os_id_to_current:

imp::current_os_id must not allocate with the global allocator or call thread::current.

Not sure that's the right place to document this.

On reordering: putting set_os_id after initializing the thread-local pointer to Arc, could help to drop these constraint, so some platforms may use std::thread::current in imp::current_os_id, I don't see any need in it beyond future-proofing, however I may be missing something.

It also wouldn't cover the DESTROYED branch of current_or_unnamed, where the handle is a temporary that never goes into CURRENT.

Comment thread library/std/src/thread/thread.rs Outdated
/// already exists by then.
pub(crate) fn set_os_id_to_current(&self) {
if let Some(os_id) = imp::current_os_id() {
let _ = self.inner.os_id.set(os_id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we ignore the return value here? There should never be multiple calls into this code, right? I'd expect we want a rtabort!(...) here if we find the value is already set, as it likely indicates something has gone wrong.

@valentynkit valentynkit Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, not today. It could happen if the parent-side fill is added later, on platforms with a by-handle query (pthread_gettid_np) the parent would fill it right after creating the thread, while the child still fills it at start. But probably there should be different approach for handling this, rather than a discarded Err, and it's out of scope for this PR.

Added rtabort! instead of ignoring error.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 8, 2026
@joboet

joboet commented Aug 8, 2026

Copy link
Copy Markdown
Member

Sorry, I should have been more clear: I meant "or" not "and" when talking about things to write in the PR description. But thank you for putting in so much care!

Drop the unused name parameter from `Thread::new_current`, abort if the
OS id is set twice, and stop promising uniqueness of os_id in the docs.
@valentynkit

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants