The 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.