Split off from Ability to determine if current thread is the main thread of process (cc @LunarLambda). (Likely not going to actually submit, but providing a draft for someone more motivated to do so.)
Problem statement
Provide a standardized way of identifying threads with special semantics assigned by the runtime and/or OS. The only portable special thread is the one which runs the binary entrypoint (main
). OS-specific thread queries are available under std::os
.
Motivating use cases
Sometimes OS APIs are restricted to being called from specific threads. Normally thread locality in Rust should be handled by handle types which are not Send
, and thus cannot be transferred across thread boundaries. Unfortunately, this isn't always practical, such as with zero-argument functions without a clear preexisting context to tie a non-Send
token to.
The primary example of this is windowing. On some reasonably relevant targets, it is impossible to create a windowing event loop except on the expected thread. In order to expose a more consistent API across platforms, winit chooses to restrict creating an event loop to the main thread on all platforms, with target-specific extensions to relax that check to the one actually required by the target.
Usually the preferable way to run some code on the event loop thread from any thread is by running a callback on the event loop (e.g. event_loop.spawn(callback)
). This obviously isn't an option when creating the event loop. Similarly, helpers expecting to run on the event loop thread would generally prefer to consistently panic if run on the wrong thread (e.g. assert!(is_main_thread())
) instead of returning OS-specific error conditions from OS-dependent subsets of functionality.
Solution sketch
Presented roughly in rustdoc format.
Function
std::thread::main
pub fn main() -> Option<Thread>
Gets a handle to the process main thread.
The main thread is the thread which is used to execute the program's
main
entrypoint and shuts down the program execution when it terminates independent of any other threads.This function only returns
Some
when the programmain
is defined in Rust. Additionally, it may returnNone
even whenmain
is defined in Rust, such as when compiled into a system library.Because of this, it is generally preferred to use other mechanisms for thread cooperation when possible, such as capturing the parent
Thread
handle in the closure spawned on new threads.OS-specific versions of this functionality that work even when the program entry is externally managed are provided in the
std::os
modules where possible.Examples
let main_thread = thread::main().unwrap(); let this_thread = thread::current(); if this_thread.id() == main_thread.id() { println!("running on the main thread {:?}", main_thread); } else { println!("running off the main thread {:?}", main_thread); println!("running on spawned thread {:?}", this_thread); }
Implementation strategy: during rt init, when the main thread name is set, store the main thread handle into a global. thread::main
then returns the value from that global. If the rt was never init, that value is None
.
In fact, this is already done to set/get the thread local record of the current thread handle. All that needs to happen extra is a full global for the main thread.
Function
std::os::linux::thread::is_main_thread
(Available on
cfg(linux)
only.)pub fn is_main_thread() -> bool
Returns
true
if the current thread is the main thread.A thread is considered the main thread if the thread id is the same as the process id. Because thread ids can get reused after a thread exits, this function may potentially return
true
for a thread started after the main thread exited. [This is only possible if the main thread is external to Rust.]
Implementation strategy: getpid() == gettid()
.
The bracketed sentence relies on a forced unwind out of Rust main
being disallowed for correctness. That is technically a property we haven't committed to yet. (Currently any forced unwinding â deallocating stack frames without running unwind handlers / drop glue â is always UB. Lack of unwind handlers in the unwound stack frames is currently considered a necessary but not sufficient condition for forced unwinding to not be UB.)
Function
std::os::windows::thread::is_gui_thread
(Available on
cfg(windows)
only.)pub fn is_gui_thread() -> bool
Determines whether the calling thread is already a GUI thread.
A non-GUI thread can be converted to a GUI thread, so returning
false
does not mean this function won't returntrue
for this thread later.
Implementation strategy: Win32::UI::WindowsAndMessaging::IsGUIThread(FALSE)
.
There may be other targets which can offer relevant OS-specific functions here, but the ACP author is not familiar enough with such targets to determine such. E.g. I think macOS/iOS exposes an isMainThread
property via ObjC message.
Alternatives
Do nothing, and leave this functionality to be provided by external crates like is-main-thread (extracted from winit over 3 years ago). The functionality provided by std::thread::main
cannot be fully replicated externally, as it requires running code before main
. E.g. on Windows, is-main-thread uses a global dynamic initializer to record the thread id of the thread which runs it to test against. This works like the proposed implementation of std::thread::main
in a static executable, but in a dynamic library the test is not against the program main's thread but the DllMain
thread instead. (For winit on Windows, the test is essentially just a lint, so this is acceptable.)
Only provide std::thread::main
, and leave it to ecosystem crates to provide the OS-specific tests. This is a reasonable alternative.
Stop testing for the main thread in the ecosystem, and test the actual OS-specific requirements that "main thread" is a proxy for, either by ecosystem crates or std::os
support. While this is "more correct," it limits the predictable portability of cross-platform libraries trying to encapsulate different targets with different requirements. "On the main thread" is commonly used as a proxy requirement because it's both simple to understand and sufficient on effectively every platform. (Though in some configurations, the "main thread" might not be able to be the literal main
entrypoint. In a perfect world, there'd be a separate target for each which can shim the actual entry to Rust main
.)