Understanding actors in Swift
Learn how to use Swift actors to prevent data races when working with concurrency.
28 Oct 2024 · 7 min read
Concurrency is a complex problem when writing software, especially when dealing with shared mutable state across different threads. With Swift's move toward structured concurrency, actors are a powerful feature introduced to make it easier to write safe and efficient concurrent code.
In this article, we'll dive into what actors are, how they differ from other types in Swift, and how they can help you avoid data races in concurrent environments.

How do Swift actors prevent data races?
A data race occurs when multiple threads access shared mutable data concurrently, and at least one thread modifies the data. This leads to unpredictable results, bugs, and crashes.
Swift's actors prevent data races through actor isolation, which ensures:
- Only one task can access or modify an actor’s mutable state at any given time.
- Synchronous access to an actor's mutable state from outside is prohibited.
- Direct modification of an actor's mutable state from outside is not allowed.
By using actors, Swift guarantees that shared mutable data is safely managed in a concurrent environment, reducing the risk of data races.
Defining an actor
Actors are represented by a reference type called actor:
actor Shop {let id = "abc"var itemsCount = 10func purchase() {itemsCount-=1}}
In this example, Shop is an actor that holds a mutable state itemsCount. The actor ensures that only one task can modify itemsCount at a time. This is the core advantage of actors over classes when it comes to handling concurrent operations.
Using async/await to access data from an actor
To interact with an actor's properties or methods from outside, we need to use async/await, marking any potential suspension points with await. Only immutable data, such as constants (id in this case), can be accessed synchronously.
let shop = Shop()print(shop.id) // Synchronous access to immutable propertyTask {await shop.purchase() // Asynchronous call to modify mutable statelet count = await shop.itemsCount // Asynchronous access to mutable state}
Even though we didn't explicitly mark purchase as an async function, we still need to use await when calling it because it interacts with the actor's mutable state. If some other task has already called purchase, our purchase() call would suspend and wait for the other one to finish.
This is part of actor isolation, which ensures that only asynchronous access to an actor's mutable state is allowed from outside.
Actor isolation
Actor isolation is the principle that protects an actor's internal state. So when accessing the data of an actor, the following rules apply:
- Synchronous access to an actor's mutable state from the outside is not allowed.
- Asynchronous access via await is required for mutable state.
- Reading immutable properties (constants) from the outside can be done synchronously.
- Internal access (within the actor’s methods) can happen synchronously.
These guarantees ensure that concurrent access to shared data is controlled and safe and are known as actor isolation.
Actor's non-isolated declarations
There may be cases where we want to allow synchronous access to certain methods or properties of an actor. For this, Swift provides the nonisolated keyword, which allows a method or property to bypass actor isolation.
actor Shop {let id = "abc"nonisolated func log(message: String) {print("\(id): \(message)")}}
In this example, the log method is declared nonisolated, allowing it to be called synchronously from outside the actor without await. Inside the nonisolated method, we are only allowed to use nonisolated properties (constants) and methods.
Handling thread safety with Sendable types
In Swift, actors are designed to protect their internal state from concurrent access, but we still need to be careful when passing data in and out of an actor. This is where the Sendable protocol comes into play. Sendable ensures that any type used in a concurrent environment is thread-safe, meaning it can safely be shared across different tasks without causing data races.
So far, we have only used value types String or Int when working with actors. By default, Swift's value types like Int and String are Sendable, but when working with custom types, it's important to ensure that they conform to Sendable.
To check the thread safety of a type we can use the Sendable protocol:
actor Shop {var owner: Owner}struct Owner: Sendable {var name: String}
Mutable classes are not thread safe:
class Owner: Sendable { // compiler error: Non-final class 'Owner' cannot conform to Sendablevar name: String}
Since mutable classes are not thread safe we get a compiler error. To avoid this, we should either mark immutable classes as final or prefer using structs, which are value types and inherently thread-safe.
Actor reentrancy
One unique feature of Swift actors is reentrancy. When an actor is suspended awaiting an async call, it can process other tasks in its queue. This improves efficiency but can lead to unexpected behavior if we’re not careful, as an actor’s state may change between async calls. Swift automatically manages reentrancy, but it’s important to be aware of this behavior when working with actors.



Newsletter
Like to support my work?
Say hi
Related tags
Articles with related topics
Latest articles and tips



