Swift 5.5 connects asynchronous control flow with state isolation
Swift 5.5 ships async functions, structured concurrency and actors. Their combined model distinguishes suspension, child task lifetime and protected state, including the reentrancy that remains at actor suspension points.
A callback can describe what happens after an operation finishes, but it leaves the surrounding control flow distributed across functions. Protecting the state those callbacks use creates another problem, usually handled through conventions about queues or locks. Swift 5.5, released on September 20, brings asynchronous functions and actors into the language alongside structured concurrency. The facilities address different parts of that problem and need to be understood together.
The change is larger than replacing nested closures with straight-line code. An asynchronous function can suspend without retaining its executing thread. An actor restricts access to its mutable state. Structured child tasks give concurrent work an enclosing lifetime. Those rules let the compiler participate in reasoning that otherwise depends heavily on the discipline of every caller.
I like Swift's value model because it helps keep mutation understandable. Actors extend that concern to state that intentionally remains shared. Their usefulness depends on knowing the exact guarantee, particularly where an asynchronous method suspends and another operation can change the actor's state.
Async functions can release their executing thread
SE-0296 describes an asynchronous function as an ordinary function that can give up its thread while waiting. The function retains the storage needed to resume later. This allows code to use normal local variables and control flow without forcing a thread to remain blocked for the whole operation.
An async declaration does not create concurrency every time the function is called. Two consecutive awaited calls still express a sequence. The first completes before execution reaches the second statement. The function's ability to suspend changes how the wait consumes execution resources, while the source still determines the order of dependent work.
await marks a potential suspension point. It does not promise that a call will actually suspend, because the callee's behavior and current conditions determine that. A value might already be available. The marker tells a reader that the surrounding assumptions must allow execution to pause.
Resumption also does not imply returning to the same operating-system thread. The proposal treats threads as an implementation mechanism. Actor-associated execution has a stronger contextual guarantee, returning to the actor for the protected portion of the function. Code must distinguish that isolation relationship from the identity of a particular thread.
This means wrapping blocking synchronous work in an async function does not make the work nonblocking. While it executes synchronous code, the function cannot simply give up the thread. The proposal explicitly discusses long computations and blocking waits as concerns that remain. Asynchrony needs cooperation from the operations being called.
An actor owns the right to access its state
SE-0306 introduces actors as reference types with isolated mutable state. Instance methods operate within that isolation by default. Calls from elsewhere must respect the actor's execution context, which can require an asynchronous call even when the method itself contains no await.
The following example keeps a compound update inside a synchronous actor method. The outside caller awaits access to that isolated operation.
actor Inventory {
private var available: Int
init(available: Int) {
self.available = available
}
func reserve(_ count: Int) -> Bool {
guard count > 0, count <= available else {
return false
}
available -= count
return true
}
}
func requestReservation(_ inventory: Inventory) async -> Bool {
await inventory.reserve(2)
}
The check and decrement contain no suspension point. Another actor operation cannot interleave between them. This is the useful unit for protecting an invariant such as preventing a successful reservation from making the count negative.
Changing the actor into an ordinary class would remove the language-enforced isolation. The same method body could then run concurrently through shared references unless some other synchronization mechanism intervened. The actor declaration therefore supplies behavior that the method's local syntax alone cannot establish.
The proposal does not allow outside code to assign directly to an actor-isolated property through an asynchronous property set. Updates belong in methods that can preserve the relationships among fields. This also avoids implying that a separate read and write form one atomic operation simply because both access the same actor.
Reentrancy divides a method at suspension points
Swift actors are reentrant. When an isolated method suspends, another operation can execute on that actor before the first resumes. Isolation prevents simultaneous execution of protected actor code, but it does not freeze actor state for the entire lifetime of an asynchronous method.
Consider adding an asynchronous authorization step to a reservation method. If the method first reads availability, awaits authorization and then decrements using the old assumption, another reservation may have changed availability during the wait. The accesses remain isolated. The larger operation can still violate its intended rule because the decision used stale state.
A suitable implementation must make the relevant check and update agree after the suspension, or record a reservation before awaiting with a defined policy for later failure. Those choices express application semantics. An actor cannot infer whether a partially completed business operation should reserve capacity or how it should compensate if an external operation fails.
The actor proposal gives several reasons for reentrancy. A non-reentrant actor can unnecessarily delay unrelated operations while one method waits for a download. Calls between actors can also form cycles where progress requires processing another message on an actor already waiting. Reentrancy allows that work to proceed without holding the actor idle for the entire asynchronous call.
I consider this tradeoff reasonable because the alternative would make many waits exclude unrelated work. It also makes each await a meaningful review point. A state observation made before suspension needs a reason to remain valid afterward, independently of whether the program has a low-level data race.
Structured children have a bounded lifetime
The release includes task groups and async let bindings. These let a function initiate child work with a lifetime constrained by its enclosing scope. A parent cannot simply leave behind an unfinished structured child when it exits that scope.
async let suits a fixed set of independently produced results. Each declaration starts a child initialization, and reading its result requires awaiting it. Task groups suit dynamically assembled work and completion-order processing. Choosing between them concerns the shape of the work as well as the convenience of the syntax.
The lifetime guarantee includes early exits. An unobserved child is cancelled and awaited when its scope ends. Cancellation requests cooperation from the child, so the enclosing operation still needs to wait for completion. This is why an early return does not necessarily make a function return immediately after its own result becomes known.
That cleanup introduces an implicit suspension point for async let scope exit. It is an accepted exception to the ordinary visible-await rule. A reader must account for the bindings created within a scope as well as explicit awaited calls when understanding where execution may wait.
Values crossing isolation need their own safety argument
Actor isolation would be incomplete if a method could freely return an unsynchronized mutable reference to internal state. The recipient would gain another route to mutate the object. The actors proposal therefore connects cross-actor arguments and results to the sendability rules for shared values.
Value-semantic data fits that model when its contents support safe transfer. A structure containing an ordinary mutable reference needs further consideration. Copying the outer structure copies that reference, which leaves the underlying object shared. Actor references themselves support transfer because their operations retain the actor's isolation discipline.
This compositional requirement matters when defining an actor's public methods. A convenient return type can expose a state relationship that the actor was meant to protect. Returning an appropriate value snapshot makes the intended ownership clearer. A synchronized reference type can also be valid when its own implementation supplies the required guarantee.
Protocol requirements also interact with isolation. An ordinary synchronous requirement does not give a caller a way to wait for entry to an actor. An asynchronous requirement does, so an actor can implement it while preserving isolated access. The proposal additionally defines an Actor protocol for abstractions that intentionally require actor behavior. A generic API can use that constraint when isolated operations are part of its contract. This avoids assuming that every type with a similarly named method offers the same execution guarantees. The required call behavior needs to be expressed in the interface that callers actually use.
The result is a concurrency model with several explicit boundaries. Async functions describe where work may suspend. Actor methods describe access to shared state, and structured children describe how long initiated work can remain active. Application invariants still require decisions about reentrancy and failure, but Swift 5.5 now provides language constructs with which those decisions can be expressed and checked.