Actors in Swift

Concurrency in programming introduces challenges when it comes to data sharing across different tasks. Without proper safeguards, concurrent access to shared data can lead to issues such as data races,

article

Concurrency in programming introduces challenges when it comes to data sharing across different tasks. Without proper safeguards, concurrent access to shared data can lead to issues such as data races, where the outcome depends on the timing of the tasks. To address these challenges, Swift introduces actors—a powerful feature designed to ensure safe data sharing in concurrent code. In this article, we’ll explore what actors are, how they work, and how to use them effectively in Swift.


What is an Actor?


An actor in Swift is a reference type that protects its mutable state from data races by ensuring that only one task can access that state at a time. This makes actors an essential tool for writing safe and reliable concurrent code. Unlike classes or structs, which do not inherently protect against concurrent access, actors provide built-in synchronization, making it easier to work with shared data in a concurrent environment.


Think of an actor as a gatekeeper for its internal state. Whenever a task wants to interact with the data inside an actor, it must go through the actor, which ensures that only one task is modifying the data at any given time. This prevents data races and ensures that your application behaves predictably, even under heavy concurrent workloads.


Declaring and Using Actors


Declaring an actor in Swift is straightforward and similar to declaring a class or struct. However, the main difference is that all state-modifying methods and properties within an actor are protected by the actor’s concurrency model. Here’s an example of how to declare and use an actor in Swift:



actor Counter {
    private var value = 0

func increment() {
    value += 1
}

func getValue() -> Int {
    return value
}
}

let counter = Counter()
await counter.increment()
let currentValue = await counter.getValue()

In this example, we define an actor named Counter with a private value property and two methods: increment and getValue. The increment method safely increases the value by 1, while the getValue method returns the current value. Both methods are accessed using await to ensure that the calls are properly synchronized.


How Actors Ensure Thread Safety


Actors ensure thread safety by serializing access to their mutable state. This means that even if multiple tasks try to access or modify the actor’s state simultaneously, the actor will only allow one task to proceed at a time. The other tasks will wait in line until the actor becomes available.


This built-in synchronization provided by actors simplifies the process of writing safe concurrent code. Without actors, you would need to manually synchronize access to shared data using locks or other synchronization primitives, which can be error-prone and difficult to manage. Actors handle this complexity for you, reducing the risk of introducing bugs related to data races.


Actors and Isolation


An important concept related to actors is isolation. In Swift, actors isolate their state from the rest of the code, meaning that the state of an actor can only be accessed from within that actor or through methods provided by the actor. This isolation ensures that the actor’s state is always protected and cannot be inadvertently modified by external code.


For example, consider the following code:



actor BankAccount {
    private var balance: Double = 0.0

func deposit(amount: Double) {
    balance += amount
}

func withdraw(amount: Double) -> Bool {
    guard balance >= amount else {
        return false
    }
    balance -= amount
    return true
}

func getBalance() -> Double {
    return balance
}
}

In this example, the BankAccount actor isolates its balance property, ensuring that it can only be modified through the deposit and withdraw methods. External code cannot directly access or modify the balance, which prevents accidental changes and ensures that all modifications follow the business logic defined within the actor.


Global Actors


In addition to regular actors, Swift also introduces the concept of global actors. A global actor is a singleton actor that provides global, thread-safe access to a shared resource. Global actors are useful when you need to ensure that certain tasks are always executed on a specific actor, such as updating a user interface or accessing shared resources across different parts of an application.


Global actors can be declared using the @GlobalActor attribute, and tasks can be associated with a global actor using the @MainActor attribute, which ensures that those tasks are executed on the main thread.


Conclusion


Actors are a powerful tool in Swift’s concurrency model, providing a simple and effective way to ensure safe data sharing in concurrent code. By serializing access to their mutable state and isolating that state from the rest of the code, actors help prevent data races and make it easier to write reliable and predictable concurrent applications. Understanding and using actors is essential for any Swift developer working with concurrency.


Actors in Swift ensure safe data sharing in concurrent code by serializing access to their mutable state and isolating that state from external code. This prevents data races and simplifies writing safe and reliable concurrent applications.

instructor

Exodai INSTRUCTOR!

Johan t'Sas

Owner and Swift developer!