Yes, every program has a main/initial thread, lots of UI frameworks require accessing UI elements only from the threads they're created on (often the main thread), and a run/event loop keeps the process from exiting.
In a Swift executable the main run loop is going to be handled by Swift Concurrency / Dispatch, which is what most are used to and where @MainActor is going to work. In @edwardd20 and my cases, we have Swift being loaded as a dynamic library by another runtime with its own event loop.
For all intents and purposes, an event loop implementation is like this:
class Application {
var events: [Event] = []
func exec() {
while true {
while !events.isEmpty {
let event = events.removeFirst()
switch event {
case .mouseMove:
...
case .keyPress:
...
...
case .exit:
return
}
}
}
}
}
The main thread is always busy in that loop so that the application doesn't shut down. There's no opportunity for a call to @MainActor to actually run because the main thread is always in that loop. So the solution is to use the event system to run your code instead of relying on the @MainActor.
For Qt, this is what QMetaObject::invokeMethod does. It puts a new event in the queue to run an arbitrary function. I would hope that .NET has a similar mechanism.
Yes, but with the way the current implementation works, the first synchronous part of a program runs inline, and starting the main thread’s run loop at that point effectively prevents dispatchMain() from ever being called. I wouldn't necessarily rely on this behavior if it can be avoided, but it is somewhat neat and may be the simplest workaround in certain cases.
This isn’t true on Windows, yet it is one of the assumptions I keep seeing in discussions about Swift Concurrency.
Windows is a highly multithreaded environment. In Win32, every window can be associated with a different thread. (Really it’s the message queue that is associated with the thread.) This flexibility extends to .NET WinForms, but not UWP or WinUI.
COM expands upon the OS’s underlying model by adding the notion of “apartments” that are themselves either single- or multi-threaded. And over time the kernel has grown new features like threadpools and fibers. I haven’t fleshed it out completely yet, but effective use of Swift Concurrency on Windows probably involves using a lot more non-global executors than on Darwin.
Of course, multiple windows can be associated with the same thread, and in the simplest case they are all associated with the process’s initial thread. But there’s no guarantee that thread survives beyond the call to WinMain(). Analogous to dispatch_main() on Darwin, the initial thread can exit while the process continues running. The thread isn’t blocked or looping; it has truly ceased to exist.