December 13, 2021·4 min read·engineeringswiftconcurrencyinitialization

Swift 5.5.2 makes asynchronous main setup run before queued work

Swift 5.5.2 implements revised asynchronous main semantics. The entry function runs synchronously until suspension and inherits MainActor isolation, preserving an initialization interval before queued work proceeds.

Program initialization can fail if work queued by a global initializer runs before the main function performs its setup. That ordering becomes possible when the asynchronous main function is itself placed on the queue behind work already submitted by an initializer. Swift 5.5.2 includes a targeted change to that behavior through revised asynchronous main semantics.

Evan Wilde's SE-0323, marked implemented in Swift 5.5.2, changes how the entry function starts. It runs synchronously up to the first suspension point and is implicitly protected by MainActor. The December maintenance release therefore includes a concrete adjustment to the concurrency model introduced in the 5.5 series.

I find this change useful because startup order should be explainable from the program's entry point. An asynchronous main function ought to allow setup before work that depends on that setup. The proposal identifies a specific case where simply wrapping main in a queued task broke that expectation.

An initializer can enqueue work before main exists

The motivating example uses a global C++ object's constructor to enqueue an operation on the main queue. The operation expects an audio device handle to have been initialized by main. With the earlier asynchronous-main behavior, the compiler-generated entry point enqueues the developer's main function after that operation.

When queue processing begins, the operation can execute first and observe an uninitialized handle. The problem is not that the initializer failed to enqueue its work. Both pieces of work reached the queue, but their order disagreed with the intended setup sequence. The program has a dependency that the asynchronous entry wrapper failed to preserve.

A synchronous main function behaves differently in the comparison described by the proposal. It begins executing after global initialization and can perform setup before allowing queued tasks to run. The revised asynchronous-main behavior restores an equivalent interval at the beginning of the function, while retaining the ability to suspend afterward.

The compiler starts the first continuation directly

SE-0323 explains the implementation through a simplified continuation model. An asynchronous function has an entry portion and additional portions reached after suspension. The earlier program entry enqueues the first portion and then begins draining queued work. The new entry calls the first portion directly before starting that queue-processing phase.

The change does not require making the entire asynchronous main function synchronous. Its initial statements run directly until execution reaches a suspension point. The remaining asynchronous work then follows the concurrency system's scheduling. This gives setup a defined opportunity without holding the main execution context through every later wait.

The relevant boundary is suspension. Putting an awaited operation between two initialization statements can allow queued work to observe the state established so far. The new rule therefore supports a coherent initial setup phase, but it does not make arbitrary initialization spread across awaited calls indivisible.

That distinction is visible in the source without inventing a separate lifecycle API. Operations before suspension form the initial phase. After suspension, other work may have run. A program with several dependent resources can keep the setup that must precede queued work together before its first asynchronous dependency.

MainActor isolation keeps startup on the required context

Running the initial portion directly is only part of the design. Main must also use MainActor so that entering another execution context does not introduce an unexpected initial hop. The proposal makes that isolation implicit for an asynchronous main function.

As a result, main can synchronously access declarations protected by MainActor. It need not introduce an await solely to enter that same actor. This aligns the language's isolation model with the startup behavior being promised, rather than depending only on the fact that process entry begins on a particular thread.

The rule also prevents annotating main with an unrelated global actor. Such a declaration would conflict with the required main execution context. The proposal identifies this as a source compatibility effect, along with warnings for await expressions that become unnecessary when accessing MainActor operations from main.

An existing caller that directly calls an asynchronous main function still needs await. The declaration remains asynchronous. The revised isolation can affect the context entered by that call, but it does not provide a general bridge that lets arbitrary synchronous code invoke asynchronous functions without an asynchronous caller.

The alternative would add a second setup function

The proposal considers a separate synchronous setup method followed by asynchronous main. That design would make the initialization phase explicit and prevent an accidental await within it. It would also give programs another lifecycle function to implement for a behavior that ordinary synchronous main already expresses directly.

The accepted design keeps the existing main syntax and changes its initial execution semantics. Its release-tag changelog records MainActor isolation and synchronous execution up to the first suspension. The compiler can implement these changes without requiring a runtime change, according to the proposal.

I prefer the retained single entry function because the ordering rule can be stated locally. Setup that queued work requires belongs before the first suspension. Once main suspends, the program must allow other work to observe the initialized state and proceed under the normal concurrency rules.