Concurrency, Ownership and Synchronization in Swift 6

The release of Swift 6 in September 2024 represents a pivotal moment in programming language evolution in app development, marking Apple and Swift community’s another step toward incorporating ownership and borrowing concepts traditionally associated with Rust. This convergence of ideas reflects a broader industry trend toward memory safety and data-race prevention also in application-level development, demonstrating how successful programming language features naturally propagate across ecosystems.

Swift 6’s new concurrency system relies on a rigorous data-race safety model: overlapping access to shared mutable state in concurrent contexts is now always a compile-time error rather than a warning. This dramatically strengthens Swift’s memory safety guarantees, extending them from issues like use-after-free and out-of-bounds access to fully encompass data races. With strict concurrency enabled, legacy patterns that once merely generated warnings or runtime bugs will cause the build to fail unless all shared state is isolated—typically by actors or explicitly marked as Sendable for thread crossing.

Old history for the synchronization in Swift

NSLock (iOS 2.0+)

Traditionally, Objective-C and Swift relied on NSLock to synchronize access between threads, ensuring that only one thread could enter a critical section of code at a time. NSLock is a thin wrapper around the lower-level POSIX pthread_mutex, providing an easier and more object-oriented API for mutual exclusion locking in Cocoa and CocoaTouch apps. It offers direct, low-overhead locking, but requires careful discipline to prevent subtle bugs (which always happens). Luckily, we now have some better tools for dealing with these problems:)

DispatchQueue and DispatchSemaphore (iOS 8.0+)

DispatchQueue is essentially a industry standard for high-level synchronization; it’s a FIFO queue to which you can submit tasks. DispatchQueue itself manages the execution of tasks on a pool of threads managed by the system, ensuring efficient use of system resources. This abstraction allows developers to focus on task execution rather than the underlying thread management.

Region-based isolation: Guaranteed safety

Swift 6 brings region-based isolation and the sending keyword for explicit ownership management. The compiler statically checks that, once a non-Sendable value crosses to a new isolate (e.g., from one actor to another), any future access within the original region produces a compile-time error, effectively providing move semantics and preventing aliasing across concurrency domains.

This is directly analogous to how Rust’s borrow checker and ownership system prevent both mutable and immutable references from being used unsafely in concurrent code.

Actor and @MainActor

If a property can be modified from more than one thread, your code risks being in an inconsistent state. Actors isolate properties to prevent this possibility. Actor is Swift’s concept to protecting mutable state in the objects in concurrent environments; it is a reference type (similar to classes) whose state is isolated, and only one task can access its mutable state at once.

  • An Actor’s non-mutable properties defined by let are directly accessible from outside the Actor.
  • If an Actor instance has a mutable property defined by var, only that specific instance (self) can modify it.

  • The code outside the Actor can access an the mutable property, but this access is asynchronous. That is, the code must use await.
  • The code outside the Actor can call the methods, but this access is also asynchronous. That is, the code must use await.

This is conceptually parallel to Rust’s message-passing concurrency via channels, but integrates into Swift’s type system and asynchronous runtime, using async/await.

By default, an actor’s code runs on a background thread. The distinction between the main thread and background threads is important, and this is achieved through the concept of an “actor.”

In Swift, actors help you execute code on background threads and play a crucial role in safely executing multithreaded code. The biggest challenge in multithreaded code is shared state.

This concept is extremely simillar to Rust’s move semantics and borrow checker: they allows data to transfer between concurrency domains only if the compiler can prove that the value is never accessed again in the sending context, thus guaranteeing no risk of concurrent mutation or data races.