Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions library/std/src/thread/current.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ pub(crate) fn current_or_unnamed() -> Thread {
(*current).clone()
}
} else if current == DESTROYED {
Thread::new(id::get_or_init(), None)
let id = id::get_or_init();
Thread::new_current(id, None)
} else {
init_current(current)
}
Expand Down Expand Up @@ -291,7 +292,7 @@ fn init_current(current: *mut ()) -> Thread {
CURRENT.set(BUSY);
// If the thread ID was initialized already, use it.
let id = id::get_or_init();
let thread = Thread::new(id, None);
let thread = Thread::new_current(id, None);

// Make sure that `crate::rt::thread_cleanup` will be run, which will
// call `drop_current`.
Expand Down
4 changes: 4 additions & 0 deletions library/std/src/thread/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ impl ThreadInit {
rtabort!("current thread handle already set during thread spawn");
}

// The handle was created by the spawning thread, so only now that we are
// running can the OS id be filled in.
self.handle.set_os_id_to_current();

if let Some(name) = self.handle.cname() {
imp::set_name(name);
}
Expand Down
13 changes: 13 additions & 0 deletions library/std/src/thread/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,19 @@ fn test_thread_os_id_not_equal() {
assert!(current_id != spawned_id);
}

#[test]
fn test_thread_os_id_matches_current() {
assert_eq!(thread::current().os_id(), crate::sys::thread::current_os_id());
}

#[test]
fn test_thread_os_id_of_spawned_thread() {
let spawned = thread::spawn(|| thread::current().os_id());
let handle = spawned.thread().clone();
let spawned_id = spawned.join().unwrap();
assert_eq!(handle.os_id(), spawned_id);
}

#[test]
fn test_scoped_threads_drop_result_before_join() {
let actually_finished = &AtomicBool::new(false);
Expand Down
57 changes: 56 additions & 1 deletion library/std/src/thread/thread.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ use crate::alloc::System;
use crate::ffi::CStr;
use crate::fmt;
use crate::pin::Pin;
use crate::sync::Arc;
use crate::sync::{Arc, OnceLock};
use crate::sys::sync::Parker;
use crate::sys::thread as imp;
use crate::time::Duration;

// This module ensures private fields are kept private, which is necessary to enforce the safety requirements.
Expand Down Expand Up @@ -49,6 +50,7 @@ use thread_name_string::ThreadNameString;
struct Inner {
name: Option<ThreadNameString>,
id: ThreadId,
os_id: OnceLock<u64>,
parker: Parker,
}

Expand Down Expand Up @@ -103,13 +105,34 @@ impl Thread {
let ptr = Arc::get_mut_unchecked(&mut arc).as_mut_ptr();
(&raw mut (*ptr).name).write(name);
(&raw mut (*ptr).id).write(id);
(&raw mut (*ptr).os_id).write(OnceLock::new());
Parker::new_in_place(&raw mut (*ptr).parker);
Pin::new_unchecked(arc.assume_init())
};

Thread { inner }
}

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

let thread = Thread::new(id, name);
thread.set_os_id_to_current();
thread
}

/// Records the OS id of the calling thread in this handle.
///
/// May only be called from the thread to which this handle belongs. A
/// 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.

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.

}
}

/// Like the public [`park`], but callable on any handle. This is used to
/// allow parking in TLS destructors.
///
Expand Down Expand Up @@ -204,6 +227,38 @@ impl Thread {
self.inner.id
}

/// Gets the id the operating system gave this thread, if it has one that can
/// be read.
///
/// 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, the thread has not started
/// running yet, or the id could not be read.
///
/// 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.

///
/// # Examples
///
/// ```
/// #![feature(thread_os_id)]
/// use std::thread;
///
/// let spawned = thread::spawn(|| thread::current().os_id()).join().unwrap();
/// if spawned.is_some() {
/// assert_ne!(spawned, thread::current().os_id());
/// }
/// ```
#[unstable(feature = "thread_os_id", issue = "160215")]
#[must_use]
pub fn os_id(&self) -> Option<u64> {
self.inner.os_id.get().copied()
}

/// Gets the thread's name.
///
/// For more information about named threads, see
Expand Down
Loading