From c496571859ce0abc51abb90fe9229120d04b70e8 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Thu, 30 Jul 2026 13:41:39 +0300 Subject: [PATCH 1/6] Implement `Thread::os_id` --- library/std/src/thread/current.rs | 5 +- library/std/src/thread/lifecycle.rs | 4 ++ library/std/src/thread/tests.rs | 13 ++++ library/std/src/thread/thread.rs | 96 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) diff --git a/library/std/src/thread/current.rs b/library/std/src/thread/current.rs index 508e35cefe88f..cd2b046f91293 100644 --- a/library/std/src/thread/current.rs +++ b/library/std/src/thread/current.rs @@ -246,7 +246,9 @@ pub(crate) fn current_or_unnamed() -> Thread { (*current).clone() } } else if current == DESTROYED { - Thread::new(id::get_or_init(), None) + let thread = Thread::new(id::get_or_init(), None); + thread.set_os_id_to_current(); + thread } else { init_current(current) } @@ -292,6 +294,7 @@ fn init_current(current: *mut ()) -> Thread { // If the thread ID was initialized already, use it. let id = id::get_or_init(); let thread = Thread::new(id, None); + thread.set_os_id_to_current(); // Make sure that `crate::rt::thread_cleanup` will be run, which will // call `drop_current`. diff --git a/library/std/src/thread/lifecycle.rs b/library/std/src/thread/lifecycle.rs index d3a97bbf08fa2..241f63d37f832 100644 --- a/library/std/src/thread/lifecycle.rs +++ b/library/std/src/thread/lifecycle.rs @@ -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); } diff --git a/library/std/src/thread/tests.rs b/library/std/src/thread/tests.rs index 78b6f7c35e8db..0b2b96772279b 100644 --- a/library/std/src/thread/tests.rs +++ b/library/std/src/thread/tests.rs @@ -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 seen_by_the_thread_itself = spawned.join().unwrap(); + assert_eq!(handle.os_id(), seen_by_the_thread_itself); +} + #[test] fn test_scoped_threads_drop_result_before_join() { let actually_finished = &AtomicBool::new(false); diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index 7c9c91c3b0c78..a791f2147dbee 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -6,6 +6,7 @@ use crate::fmt; use crate::pin::Pin; use crate::sync::Arc; 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. @@ -40,6 +41,59 @@ mod thread_name_string { 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". +// +// Of the platform calls behind `current_os_id`, only Apple's yields a `uint64_t`, +// and every Apple target has 64-bit atomics, so `usize` loses nothing on the +// second arm. +cfg_select! { + target_has_atomic = "64" => { + use crate::sync::atomic::{Atomic, AtomicU64, Ordering::Relaxed}; + + struct OsId(Atomic); + + impl OsId { + const fn unknown() -> Self { + Self(AtomicU64::new(0)) + } + + fn get(&self) -> Option { + match self.0.load(Relaxed) { + 0 => None, + id => Some(id), + } + } + + fn set(&self, id: u64) { + self.0.store(id, Relaxed); + } + } + } + _ => { + use crate::sync::atomic::{Atomic, AtomicUsize, Ordering::Relaxed}; + + struct OsId(Atomic); + + impl OsId { + const fn unknown() -> Self { + Self(AtomicUsize::new(0)) + } + + fn get(&self) -> Option { + match self.0.load(Relaxed) { + 0 => None, + id => Some(id as u64), + } + } + + fn set(&self, id: u64) { + self.0.store(id as usize, Relaxed); + } + } + } +} + /// The internal representation of a `Thread` handle /// /// We explicitly set the alignment for our guarantee in Thread::into_raw. This @@ -49,6 +103,7 @@ use thread_name_string::ThreadNameString; struct Inner { name: Option, id: ThreadId, + os_id: OsId, parker: Parker, } @@ -103,6 +158,7 @@ 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(OsId::unknown()); Parker::new_in_place(&raw mut (*ptr).parker); Pin::new_unchecked(arc.assume_init()) }; @@ -110,6 +166,17 @@ impl Thread { Thread { inner } } + /// 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() { + self.inner.os_id.set(os_id); + } + } + /// Like the public [`park`], but callable on any handle. This is used to /// allow parking in TLS destructors. /// @@ -204,6 +271,35 @@ 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 `ps`, `top`, a debugger or a crash log shows, unlike + /// [`ThreadId`], which is internal to Rust and unrelated to it. `None` means + /// the platform has no such id or offers no way to read it, or that the + /// thread has not started running yet. + /// + /// 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. + /// + /// # Examples + /// + /// ``` + /// #![feature(thread_os_id)] + /// use std::thread; + /// + /// let spawned = thread::spawn(|| thread::current().os_id()); + /// println!("spawned thread ran as {:?}", spawned.join().unwrap()); + /// ``` + #[unstable(feature = "thread_os_id", issue = "160215")] + #[must_use] + pub fn os_id(&self) -> Option { + self.inner.os_id.get() + } + /// Gets the thread's name. /// /// For more information about named threads, see From 84d2f10e28ff07f057497664bdb093ddde5bbc4d Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Thu, 30 Jul 2026 17:54:25 +0300 Subject: [PATCH 2/6] Store the OS thread id in a `OnceLock` --- library/std/src/thread/thread.rs | 71 ++++---------------------------- 1 file changed, 9 insertions(+), 62 deletions(-) diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index a791f2147dbee..2509c87a96182 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -4,7 +4,7 @@ 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; @@ -41,59 +41,6 @@ mod thread_name_string { 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". -// -// Of the platform calls behind `current_os_id`, only Apple's yields a `uint64_t`, -// and every Apple target has 64-bit atomics, so `usize` loses nothing on the -// second arm. -cfg_select! { - target_has_atomic = "64" => { - use crate::sync::atomic::{Atomic, AtomicU64, Ordering::Relaxed}; - - struct OsId(Atomic); - - impl OsId { - const fn unknown() -> Self { - Self(AtomicU64::new(0)) - } - - fn get(&self) -> Option { - match self.0.load(Relaxed) { - 0 => None, - id => Some(id), - } - } - - fn set(&self, id: u64) { - self.0.store(id, Relaxed); - } - } - } - _ => { - use crate::sync::atomic::{Atomic, AtomicUsize, Ordering::Relaxed}; - - struct OsId(Atomic); - - impl OsId { - const fn unknown() -> Self { - Self(AtomicUsize::new(0)) - } - - fn get(&self) -> Option { - match self.0.load(Relaxed) { - 0 => None, - id => Some(id as u64), - } - } - - fn set(&self, id: u64) { - self.0.store(id as usize, Relaxed); - } - } - } -} - /// The internal representation of a `Thread` handle /// /// We explicitly set the alignment for our guarantee in Thread::into_raw. This @@ -103,7 +50,7 @@ cfg_select! { struct Inner { name: Option, id: ThreadId, - os_id: OsId, + os_id: OnceLock, parker: Parker, } @@ -158,7 +105,7 @@ 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(OsId::unknown()); + (&raw mut (*ptr).os_id).write(OnceLock::new()); Parker::new_in_place(&raw mut (*ptr).parker); Pin::new_unchecked(arc.assume_init()) }; @@ -173,7 +120,7 @@ impl Thread { /// already exists by then. pub(crate) fn set_os_id_to_current(&self) { if let Some(os_id) = imp::current_os_id() { - self.inner.os_id.set(os_id); + let _ = self.inner.os_id.set(os_id); } } @@ -274,10 +221,10 @@ impl Thread { /// Gets the id the operating system gave this thread, if it has one that can /// be read. /// - /// This is the id `ps`, `top`, a debugger or a crash log shows, unlike - /// [`ThreadId`], which is internal to Rust and unrelated to it. `None` means - /// the platform has no such id or offers no way to read it, or that the - /// thread has not started running yet. + /// 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, + /// or that the thread has not started running yet. /// /// 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 @@ -297,7 +244,7 @@ impl Thread { #[unstable(feature = "thread_os_id", issue = "160215")] #[must_use] pub fn os_id(&self) -> Option { - self.inner.os_id.get() + self.inner.os_id.get().copied() } /// Gets the thread's name. From 3a3ff338589edd5392aca0b033150052a2286afd Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 2 Aug 2026 13:55:32 +0300 Subject: [PATCH 3/6] Add `Thread::new_current` for current-thread handles --- library/std/src/thread/current.rs | 8 +++----- library/std/src/thread/thread.rs | 9 +++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/library/std/src/thread/current.rs b/library/std/src/thread/current.rs index cd2b046f91293..a452e5d4ef89a 100644 --- a/library/std/src/thread/current.rs +++ b/library/std/src/thread/current.rs @@ -246,9 +246,8 @@ pub(crate) fn current_or_unnamed() -> Thread { (*current).clone() } } else if current == DESTROYED { - let thread = Thread::new(id::get_or_init(), None); - thread.set_os_id_to_current(); - thread + let id = id::get_or_init(); + Thread::new_current(id, None) } else { init_current(current) } @@ -293,8 +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); - thread.set_os_id_to_current(); + let thread = Thread::new_current(id, None); // Make sure that `crate::rt::thread_cleanup` will be run, which will // call `drop_current`. diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index 2509c87a96182..0ef7e8f458116 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -113,6 +113,15 @@ impl Thread { 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) -> Thread { + 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 From 5c8cb4ee6de75bdade0c9f49670641f278e848d9 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 2 Aug 2026 13:59:03 +0300 Subject: [PATCH 4/6] Reword the `os_id` docs after review --- library/std/src/thread/thread.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index 0ef7e8f458116..e195c8567a420 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -232,14 +232,15 @@ impl Thread { /// /// 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, - /// or that the thread has not started running yet. + /// it. `None` means the platform has no such id, the thread has not started + /// running yet, or the id could not be read. /// - /// 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. + /// 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. /// /// # Examples /// @@ -247,8 +248,10 @@ impl Thread { /// #![feature(thread_os_id)] /// use std::thread; /// - /// let spawned = thread::spawn(|| thread::current().os_id()); - /// println!("spawned thread ran as {:?}", spawned.join().unwrap()); + /// 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] From 42cdf8cd2322789995768c4b47b87e900778c8e0 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Sun, 2 Aug 2026 14:42:46 +0300 Subject: [PATCH 5/6] Rename the spawned id binding in the `os_id` test --- library/std/src/thread/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/library/std/src/thread/tests.rs b/library/std/src/thread/tests.rs index 0b2b96772279b..d7f4a47cb4a47 100644 --- a/library/std/src/thread/tests.rs +++ b/library/std/src/thread/tests.rs @@ -365,8 +365,8 @@ fn test_thread_os_id_matches_current() { fn test_thread_os_id_of_spawned_thread() { let spawned = thread::spawn(|| thread::current().os_id()); let handle = spawned.thread().clone(); - let seen_by_the_thread_itself = spawned.join().unwrap(); - assert_eq!(handle.os_id(), seen_by_the_thread_itself); + let spawned_id = spawned.join().unwrap(); + assert_eq!(handle.os_id(), spawned_id); } #[test] From 71be0e93f7e21b122a9752851d272e7f5837b343 Mon Sep 17 00:00:00 2001 From: Valentyn Kit Date: Mon, 10 Aug 2026 14:43:26 +0300 Subject: [PATCH 6/6] Address review feedback on `Thread::os_id` 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. --- library/std/src/thread/current.rs | 4 ++-- library/std/src/thread/thread.rs | 23 ++++++++++++++--------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/library/std/src/thread/current.rs b/library/std/src/thread/current.rs index a452e5d4ef89a..3512f04868303 100644 --- a/library/std/src/thread/current.rs +++ b/library/std/src/thread/current.rs @@ -247,7 +247,7 @@ pub(crate) fn current_or_unnamed() -> Thread { } } else if current == DESTROYED { let id = id::get_or_init(); - Thread::new_current(id, None) + Thread::new_current(id) } else { init_current(current) } @@ -292,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_current(id, None); + let thread = Thread::new_current(id); // Make sure that `crate::rt::thread_cleanup` will be run, which will // call `drop_current`. diff --git a/library/std/src/thread/thread.rs b/library/std/src/thread/thread.rs index e195c8567a420..ff6affae7f7da 100644 --- a/library/std/src/thread/thread.rs +++ b/library/std/src/thread/thread.rs @@ -116,8 +116,11 @@ impl Thread { /// 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) -> Thread { - let thread = Thread::new(id, name); + /// + /// Takes no name because passing one into `Thread::new` allocates with the + /// global allocator, which `thread::current` is documented never to use. + pub(crate) fn new_current(id: ThreadId) -> Thread { + let thread = Thread::new(id, None); thread.set_os_id_to_current(); thread } @@ -127,9 +130,14 @@ impl Thread { /// 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. + /// + /// `imp::current_os_id` must not allocate with the global allocator or call + /// `thread::current`. 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); + if self.inner.os_id.set(os_id).is_err() { + rtabort!("thread OS id already set"); + } } } @@ -235,12 +243,9 @@ impl Thread { /// 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. + /// The operating system may reuse the id of a thread that has exited, and a + /// `Thread` handle can outlive the thread it refers to. Use the id only + /// where a reused id is harmless, such as logging. /// /// # Examples ///