BBMM Technologies
← All articles
7 min readswift, concurrency, actors, ios

Swift Concurrency: Structured Tasks and Actors in Practice

By Mykhailo Boichuk · Co-founder & Vice-President

In short

Swift concurrency replaces ad hoc callbacks and queues with structured tasks and actors that the compiler can reason about. Structured tasks tie lifetime and cancellation to scope, actors serialize access to mutable state, and Sendable checking moves data races from runtime to compile time. The cost is a stricter model that surfaces isolation problems earlier and demands they be addressed.

Structure replaces loose callbacks

Before structured concurrency, asynchronous work in Swift relied on completion handlers and dispatch queues whose relationships were invisible to the compiler. Lifetimes were implicit, cancellation was manual, and errors propagated through whatever convention the team agreed on. Structured concurrency makes these relationships explicit. A child task created with async let or a task group lives within the scope of its parent, and when that scope ends, the children are awaited or cancelled.

The practical payoff is that cancellation and error handling follow the call tree rather than a tangle of stored closures. If a parent task is cancelled, its children receive cancellation. Work no longer outlives the context that needed it, which removes a common class of leaks and stale updates.

Actors serialize mutable state

An actor is a reference type that protects its mutable state by serializing access. Only one task touches an actor’s isolated state at a time, so the data races that plague shared-memory concurrency cannot occur on that state. Calls into an actor from outside are asynchronous because they may need to wait their turn.

  • Use an actor when several tasks read and write the same mutable state.
  • Use the main actor for state that drives the user interface.
  • Prefer value types and local state when isolation is not actually needed.

Actors are not free. Hopping between actors has a cost, and an interface that calls into an actor inside a tight loop can serialize work that did not need serializing. The discipline is to isolate the smallest amount of state that genuinely needs protection.

Sendable moves races to compile time

The Sendable protocol marks types that are safe to pass across concurrency boundaries. With full concurrency checking enabled, the compiler rejects code that would share non-Sendable state between tasks. This is the most disruptive part of adoption because it surfaces latent data-sharing assumptions that previously went unchecked.

Treat Sendable warnings as findings, not noise. Each one marks a place where state crossed an isolation boundary without protection. Silencing them with unchecked conformance trades a compile-time guarantee for a runtime risk.

Adopting it without a rewrite

Migration works best incrementally. Convert leaf functions to async first, introduce actors around clearly shared state, and enable stricter concurrency checking module by module rather than all at once. Each step turns a class of runtime bug into a compile error you can address deliberately.

The honest trade-off is that Swift concurrency front-loads work. It asks for isolation decisions early and refuses to compile some code that used to run. In exchange it eliminates a category of intermittent, hard-to-reproduce failures that otherwise surface only in production.

Key takeaways

  • Structured tasks tie lifetime, cancellation, and errors to lexical scope.
  • Actors serialize access to mutable state and remove data races on that state.
  • Sendable checking moves data-race detection from runtime to compile time.
  • Isolate the smallest state that needs protection to avoid serializing unnecessarily.
  • Migrate incrementally and treat concurrency warnings as real findings.

Frequently asked questions

What problem do actors solve in Swift?
They protect mutable state by serializing access, so only one task touches an actor’s isolated state at a time, which prevents data races on that state.
Why does Swift concurrency feel stricter than the old model?
Because Sendable checking surfaces data-sharing assumptions at compile time that the callback-and-queue model left unchecked until runtime.
Can a team adopt Swift concurrency gradually?
Yes. Convert leaf functions to async, add actors around shared state, and enable stricter checking module by module rather than rewriting everything at once.

References

About the author

Mykhailo Boichuk

Co-founder & Vice-President

Mykhailo is an engineer who builds native applications and the systems behind them. He concentrates on macOS and iOS performance, local-first data architecture, and the synchronization problems that come with offline-capable software.