[Book Update] What is Actor and Sendable in Swift?
These days Swift Concurrency is all the rage and you'd definitely get asked about it on iOS dev interviews. This is an excerpt from The iOS Interview Guide V2 of a question about Actors and Sendable.
This is an excerpt from the upcoming 2nd edition of my book The iOS Interview Guide. If you’re interested to hear more book updates like this and to receive draft chapters as I write them feel free to subscribe to my substack, I’ll post all the updates here.
What is Actor and Sendable in Swift?
This question will come up as part of discussion around asynchronicity on iOS and as conversation goes deeper into the topic of Swift Concurrency. In Swift Concurrency, async/await is the means of defining and executing async work, while actor is the means of protecting data from race conditions and other async data access/read problems. As an iOS engineer you need to know how to work with actors (and their advantages, limitations, etc.) and you need to know how their features compare to other solutions such as GCD, RxSwift, or alternatives in other languages such as Coroutines in Kotlin.
Expected Answer: Swift Concurrency introduced a new type called actor. It behaves and acts much like class type. It’s a reference type, but its main purpose is to protect access to its mutable state from concurrent access by different threads. This helps protect data access to the actor's state and helps prevent data races by enforcing static checks at compile time. This process is called actor isolation.
Actor isolation is a “barrier”, so to speak, of access to actor’s properties data. Essentially, in order to access data on an actor externally, read its properties or write to them or call methods that mutate its state, you have to use the await keyword and access them asynchronously. Internally, the system will queue up access to data and mutations, processing them one at a time. This serializes access, preventing race conditions and ensuring data synchronization. This isolation is enforced at compile time giving developers an early warning.
Internal data access in actors can happen in a “normal” synchronous way and doesn’t require an await call.
Sendable
To maintain isolation, any data passed into an actor's methods or returned from them must be safe to share across threads. Types that are safe to share have to conform to the Sendable protocol. Marking something as Sendable, you guarantee to the compiler that it’s safe to transfer that value across threads.
Sendable protocol is akin to Codable in a sense that in order to have a type conform to it, every property of that type needs to conform to Sendable as well. Primitive types conform to it by default, and structs and enums as well, as long as all of their properties are other primitives or types conforming to Sendable. You can also make normal class types conform to Sendable, but you’ll have to manually manage access and mutability of their own properties using locks or other thread-safe mechanisms.
By default, everything is isolated in an actor. If you want to explicitly opt-out of the default isolation for a property or a function, you’d need to mark it with the non-isolated keyword. You would opt-out these functions or properties in an actor that are not mutating any state - since they don't mutate any state, they can be accessed safely synchronously and do not need to be put on an asynchronous queue with await.
Actor Reentrancy
One important thing to remember about actors is the actor's reentrancy behavior. When one message is sent to an actor object and is awaited on, it doesn’t mean that the actor becomes exclusively locked and will only execute other messages after the first request is fully processed. Instead, while one await call is suspended, another message can be processed by the actor and potentially finish sooner than the first one. This solves the problem of deadlocks that could otherwise occur where one actor waits on the execution to finish from another actor, and the other, in turn, waits on the execution of the first one to finish, therefore causing a never-ending deadlock.
The implication of this, though, is that you can never be sure if the state of the actor hasn’t changed between before and after you call an await method on it. You will have to check the data after suspension finishes to make sure it’s still in the correct state.
Prior Solutions
Prior to actors, using GCD (Grand Central Dispatch), we had to create a private serial dispatch queue to achieve a synchronization lock that allowed only one operation at a time to access data.
With RxSwift you’d use something like BehaviorSubject with SerialDispatchQueueScheduler with a similar setup as GCD - SerialDispatchQueueScheduler will ensure that there is a synchronization lock as long as you perform your operations on that scheduler.
Either GCD or RxSwift solutions required developer’s discipline to implement accurately, with Swift Concurrency’s actors the compiler aids in identifying isolation violation and potential race conditions it could lead to.
In other languages like Kotlin, you’d use Mutex and/or StateFlow, but there is no direct compile time safe equivalent to actors.
Red Flags:
Stating actors are always single-threaded or non-reentrant: This is a major misunderstanding. While an actor processes messages serially, it is reentrant. An interviewer might ask a follow-up question specifically designed to test for this misunderstanding.
Believing nonisolated moves code off the actor: Misconception that annotating a method with nonisolated will automatically run it on a background thread. nonisolated simply removes actor isolation; it doesn't dictate the execution context. The method will run on the caller's context.
Saying Sendable for classes is easy: If the candidate suggests that making any mutable class Sendable is straightforward, it indicates a lack of understanding of the complexities of thread-safe mutable shared state and the implications of @unchecked Sendable.
Confusing async with concurrency: While async functions enable concurrency, they don't inherently run on a background thread. They only mark potential suspension points. The actual thread hopping for background work is handled by the runtime or by explicitly offloading work (e.g., to an actor or another Task).
Inability to explain why actors are safer than GCD/RxSwift for state: A strong answer shouldn't just list alternatives but articulate how actors offer a compiler-enforced safety mechanism that manual GCD queues or RxSwift schedulers do not for state isolation.
Help me make this chapter better.
What did I miss? Anything to add to make it more clear or helpful?
Your feedback directly helps shape this book into a more practical resource for the entire iOS community. Drop a comment or send me an email.