Swift evolution proposal 0304 Structured Concurrency gives a very short description of how the asynchronous main function should work.
Semantically, Swift will create a new task that will execute
main()
. Once that task completes, the program terminates.
The runtime function _runAsyncMain
currently implements this behaviour, wrapping the call to the user-specified main in a detached task, inside of which we do the try await
on the main function. Following the task creation, it calls out to _asyncMainDrainQueue
, which sets up a runloop to drain the main queue, executing the enqueued asynchronous main function task in the process.
There are some issues with this design that do not totally encompass all use-cases. Here are the ones I'm aware of:
-
Objective-C initializers may enqueue tasks onto queues before the main function runs. This means that the user-specified main function won't be enqueued first, so it will be run after the initializer tasks. This is confusing and can lead to unexpected behaviour. Instead, I propose that we run the main function up to the first suspension point synchronously and enqueue the remaining continuation. Once we hit the suspension point, we should kick off the runloop to drive the rest of the program. This provides a space where the main function will be run like a normal main function for setup purposes, before any enqueued tasks get executed. Once we hit the suspension point, any tasks enqueued are allowed to run in the order specified by the queue. This is consistent with the messaging around how
await
works; suspension points allow the runloop to tick. I have an implementation of the proposed behaviour posted on GitHub here: PR 38604. -
We don't have a consistent runloop mechanism available on all platforms.
dispatch_main
is available everywhere, but is not always sufficient.CFRunloopRun
provides more functionality, but isn't available everywhere. For the time being, I tried to balance this by checking to see ifCFRunloopRun
has been linked into the process and using that if it has, but otherwise falling back ondispatch_main
. This check is only done on Darwin. We should extend the model to allow folks to specify their own main runloop mechanism. I would like to make the main function run on the MainActor, so I think it would make sense to make this an optional function on the main actor. Then the_asyncMainDrainQueue
could check to see if it's set and call that. Otherwise it probably makes sense to fall back on the current behaviour as a default. As a change to the MainActor struct, this would need to be pushed off to a future release since I think the MainActor layout is locked down for this version. -
As a matter of cleanup, the main function should be run on the main actor/main thread. Right now we don't really make this guarantee, but it seems like it would be logical that the main function would run on the main thread.
-
Another matter of cleanup, the main task is initialized with a
Default
priority. It should instead pull the thread priority using a call togetCurrentThreadPriority
from the runtime. On Darwin, this will pick up the QoS from the context that the process is launched from. It will returnUserInitiated
on other platforms since we are running on the main thread. EDIT: This originally suggested hard-coding it toUserInitiated
for all cases, but this will likely be wrong for most processes.
This changes proposed in this pitch are succinctly stated as follows:
- The main function should run synchronously up to the first suspension point.
- The main function should be run on the main actor
- MainActor should provide a user-specifiable alternative to the default runloop behaviour.
- Make the main task pull the priority from
getCurrentThreadPriority
instead of a hard-codedDefault
priority.
I look forward to hearing your thoughts on the matter, in addition to issues that I have not thought of.
Thanks!