Ability to determine if current thread is the main thread of process

Many platforms (and subsequently libraries) impose special restrictions or requirements on the "main" thread of the process. For example in libraries like SDL, the event loop must run on the main thread of the application, often due to requirements set by platforms like Windows or Emscripten.

It would be very helpful if we can determine the main thread ID at runtime using something like std::thread::main() or std::thread::primary() or even just a boolean check like is_primary().

Looking online it seems like the easiest way for the standard library to accomplish this, is to store thread::current().id() in a global variable before calling main().

4 Likes

You can search for the is_main_thread functions scattered around winit to see how this can be done on various OSes.

Here it is, extracted to a separate crate. But shouldn't the stdlib provide this?

I don't think every OS has a concept of a main thread in the first place. For example in Redox OS it seems that all threads are equal as far as the kernel is concerned and as such relibc has to emulate the illusion of a main thread by eg explicitly cancel all background threads when the initial thread calls exit() or else they would remain running without the main thread. There are probably OSes where libc doesn't even attempt to emulate this concept. And for wasm what would you even consider the main thread in the first place? By the way it seems that the is_main_thread crate has a dummy impl for Android. And for Windows I think it is incorrectly implemented for dll's loaded at runtime. I think the .CRT$XCU function gets executed on the thread that loaded the dll, rather than the main thread.

3 Likes

Won't this also differ between wasm for web and for wasi? Just to complicate things even further.

It would still be useful to have a consistent definition of "main thread" across platforms because there are platforms where it matters (Document thread safety... · Issue #7140 · libsdl-org/SDL · GitHub)

I guess having just always returning "don't know"/None or "true" on platforms where it doesn't matter might be fine enough but for portability's sake it'd be nice to enforce it consistently wherever possible

True. Wasi-threads itself allows the host to produce randomized thread id's when spawning a thread and only provides the thread id when invoking the thread_spawn wasi method. And for wasi reactors I did expect it to be possible to invoke an exported method from the host on any thread and wouldn't expect the first thread on which a method is invoked to somehow become privileged over the others.

And for the web you could argue that the thread running javascript for a web page is the main thread, but that breaks down when you consider SharedWorker which allows sharing a thread between multiple pages and when instantiating the first thread on a Worker rather than the main thread of a web page. There is no dedicated target triple for the web though, so libstd can't even check if globalThis.document exists to check if it is running on the main thread of a web page.

And outside both the web and wasi you may for example decide to run multiple requests on different threads of the same wasm program without running a main function on any of them.

I think platform specific functions in std::os would avoid having to deal with platforms where the answer doesn't exist.

1 Like

IsTerminal in std::io - Rust says "On platforms where Rust does not know how to detect a terminal yet, this will return false". Now that's std::io, not std::os, but I remember this being a major point of discussion in the RFC for it and so I think it's considered an valid approach for "don't know"

This isn't Window's fault. Win32 supports 1 message queue per thread (think of it like thread-local storage). Each handle is associated with a message queue; all messages to that handle get inserted into that particular queue no matter which thread manipulated that handle or posted the message.

It's not really about whether it's any platform's "fault" for having such restrictions, I think it's fine, it just means that the functionality I talked about would be useful to have

2 Likes

Actually, on Android, it isn't the main thread created by the OS, but instead the thread created by SDL.

2 Likes

Storing the thread ID before calling main wouldn't work for Rust code used as a library where the main program entry point is not Rust.

2 Likes

On Windows, even if most (if not all) Win32 functionality is restricted to "on the matching message queue thread," "on the UI thread" (or "on a GUI thread"), or "on the STA thread" instead of "on the primary thread," being the primary thread is a very good proxy for "on the main window thread" that is used by approximately every framework. (Win32 docs call the thread typically created with a process the primary thread.)


That said, Rust documents under std::thread that

When the main thread of a Rust program terminates, the entire program shuts down, even if other threads are still running.

Rust has a concept of the "main" thread. A fn std::thread::main() -> Option<Thread> should return that main thread, which is known to Rust if Rust controls the program entry, and might be known to Rust if the host provides a way to determine which thread is the "main" thread, i.e. terminating that thread terminates the program without waiting for other threads to terminate[1]. Returning None would indicate that Rust did not control the entry point and the host OS does not provide a standard way to retrieve an ID for the "main" thread.

Full aside: I've considered a few times if exposing a public unsafe fn std::sys::init(...) or std::os::init (which already exists privately for the standard runtime startup) for the case when Rust isn't the process entry point would be desirable. The safety condition would be that this function must be called before any other std function if it is called, and it would provide a #[non_exhaustive] struct of options for any Rust-cached OS info (e.g. primary thread ID, argc/argv, etc).


  1. Thread locals are fun! If I recall my experimentation correctly, on unixes, terminating the main thread abruptly terminates the process without running any thread-local destructors, but on Windows, terminating the main thread (or calling process::exit) will first drop all of that thread's thread-locals and then abruptly terminate the process. Dropping thread locals on Windows includes first acquiring the loader lock, so the process termination waits for at least whatever thread is currently dropping thread-locals, if any. I don't know if fairness of the loader lock is specified, nor do I recall whether running threads get abruptly terminated upon main terminating / calling exit, or only after the exiting thread's dtors have run. ↩︎

3 Likes

Windows doesn't really have a concept of the main thread. You can set up a "primary" thread for certain things, if you like, but std should not be opinionated about that.

Yes the thread that runs main could be said to be quite literally the "main" thread but Rust might be used in a library so Rust's main is never called and it may be hard or impossible to find it after the fact. Heck, it may have been terminated already.

1 Like

This doesn't give that thread any distinguishing characteristics, though. It can be understood just as "the effect of returning from main() is to terminate the process"; but since the sole authority over when main() returns is main() itself, consulting an outside source won't ever tell you reliably useful information, because

  • main() can catch panics, and
  • any thread can abort(),

so some code learning whether it is on the main thread says nothing reliable about whether its own returning or panicking will result in terminating the process, nor any other results.

Given this, and the other remarks about how the concept of “the main thread” on Windows or Android is really “the thread running the relevant event loop” or such, I think that the status quo (with regard to std) is actually fine: when a “main thread” significance is introduced by some library or platform API, the Rust bindings for that library may wish to provide a way to identify that thread, but std shouldn't encourage code to add significance to “the main thread” on their own initiative.

Introducing a ubiquitous concept of main thread would be a step away from “fearless concurrency”; main-thread-specialness should be a rare case and motivated by pre-existing requirements. Of course, Rust does also let you write purposefully single-threaded code, but the primary mechanism for doing that is !Send, not checking thread identity.

6 Likes

Fearless concurrency is about being confident in writing multithreaded code because if you make any mistakes the compiler has your back. And today, in many platforms, it's an error to perform certain tasks in threads other than the main thread, but Rust unfortunately doesn't help with that (but it should).

This seems highly platform specific because of the reason why the main thread is special. I think if we did this it'd be better to have specific functions functions. For example, we could have std::os::macos::is_ui_thread().

It doesn't make sense to have a single, cross-platform function because many platforms don't have the concept of a main thread in the same sense and of those that do the reasons for them are different.

6 Likes

And, in situations where the distinguished thread is an event-loop thread, run_callback_on_event_loop(|| ...) is a more generally applicable operation than “is this the event loop thread?”. That's fundamentally tied to the particular kind of event loop being acted on.

(You might think “I'll check whether I'm already on the right thread, and if so, skip the callback business.” Yes, that's an option, but it's not one to be automatically preferred; subtle ordering-of-effects bugs can happen when things are sometimes done immediately and sometimes later, relative to the ordering of events/tasks on the event loop thread. Better to have only one behavior, not two.)

a lot of this feels like it's kind of going past what I mean, which are things like "SDL_Init should ONLY be called on the main() thread" and similar things (again, emscripten was listed as an example where lots of stuff can only be done on the main thread)

All I want is to be able to do

if !thread::is_primary() { return Err(NotMainThread) }

which seems fairly reasonable to me, rather than "some functions on some platforms sometimes fail if called from a thread". Right now the options are either, document everything (doesn't prevent mistakes) or let the user figure out what works.

On the other hand, winit is enforcing (or at least attempting to) an "is the main thread" requirement on all targets for EventLoop::build specifically in the name of portability. Instead of enforcing the weaker, platform-specific precondition, winit is enforcing the same, sufficient precondition on all targets, in order to minimize the chance of changing targets and suddenly having winit panic because the structure of the application is fundamentally incompatible with the target.

Attempting to create the event loop off the main thread will panic. This restriction isn’t strictly necessary on all platforms, but is imposed to eliminate any nasty surprises when porting to platforms that require it. EventLoopBuilderExt::any_thread functions are exposed in the relevant platform module if the target platform supports creating an event loop on any thread.

2 Likes