Swift and Concurrency: Synchronization
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. Actor
s 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 class
es) whose state is isolated, and only one task can access its mutable state at once.
- An
Actor
’s non-mutable properties defined bylet
are directly accessible from outside theActor
. -
If an
Actor
instance has a mutable property defined byvar
, 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 useawait
. - The code outside the
Actor
can call the methods, but this access is also asynchronous. That is, the code must useawait
.
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.