What Are Actors and How Do I Use Them?
If you are accustomed to working with single-threaded technologies, you might not immediately consider the fact that multithreading introduces new challenges, errors, and bugs. Many of these issues arise from something called data races. A data race occurs when multiple threads attempt to update or modify the same property, resulting in unexpected behaviors, crashes, and even corrupted UI.
In an effort to address this issue, Apple introduced a concept known as Actors in Swift 5.5 Actors function much like classes, with a few distinctions. Notably, an Actor cannot be marked as “ final ” and it does not support the “ override ” keyword. Moreover, actors can only be accessed by a single thread at a time.
Let’s delve into how Actors operate through an illustrative example. (You can find all the code corresponding to this blog post in the following GitHub Repository)
public actor PendingPayment {
public var amount = 20.99
var paid = false
public func hasBeenPaid() {
if !paid {
print("Transaction need to be paid \(amount)")
} else {
print("Transaction already paid")
}
}
public func pay(from account: BankAccount) -> Bool {
var paymentProcessed = false
if !paid {
paid = true
account.money -= amount
paymentProcessed = true
}
return paymentProcessed
}
public init(amount: Double = 20.99, paid: Bool = false) {
self.amount = amount
self.paid = paid
}
}
We have multiple things going on on this actor, the most important is the pay method, that takes a bank account as a parameter and executes a payment if the pending amount hasn’t been settled.
let pedroBankAccount = BankAccount(money: 200)
let jaimeBankAccount = BankAccount(money: 199)
let newPayment = PendingPayment()
Task {
await print(newPayment.amount)
}
Task {
print("Pedro has paid: \(await newPayment.pay(from: pedroBankAccount))")
}
Task {
print("Jaime has paid: \(await newPayment.pay(from: jaimeBankAccount))")
}
//Output
20.99
Pedro has paid: true
Jaime has paid: false
Now, let’s simulate a scenario where two distinct bank accounts attempt to settle the same pending payment simultaneously. What unfolds every time is that Pedro is invariably the one who settles the payment. This predictability stems from the fundamental mechanism of Actors, they establish an execution queue, instead of executing all calls to the “pay” method concurrently, risking a data race, Actors execute each call sequentially, adhering to the order of their arrival. In this instance, Pedro takes precedence by arriving first.
It’s noteworthy that even in the absence of explicitly declaring any of the methods within the PendingPayment actor as asynchronous, the usage of await is mandatory when invoking any of its methods or accessing public properties.
In situations where Actors are not employed, the outcome is fraught with uncertainty. Without the orderly execution enforced by Actors, it becomes impossible to ascertain who will be charged. Worse yet, there exists the risk of both accounts being charged
The Main Actor and Thread-Safe Actions.
Now that you understand how Actors work, let’s dive into the UI of our application. The UI, much like the PendingPayment actor in the previous example, is vulnerable to multiple threads accessing and modifying it. Maybe you've come across this issue in an app before (the OS doesn't matter for this concept). You're using the app as intended, without any unexpected actions, and suddenly, the app crashes seemingly without a clear reason. But why? It's a bit tricky to explain, but in many cases, the issue is UI corruption. This usually happens when more than one thread tries to update a property or UI component. These crashes are quite challenging to figure out and fix. That's why building code that's safe for multiple threads from the start is crucial. So, how do you create code that is thread safe?
The fundamental philosophy of thread safe code for mobile applications is “All the actions, that will alter the UI should be performed on the same thread as the UI” which in the case of iOS is the main thread, or main actor.
var candy = "Chocolate"
func changeCandy(_ new_candy: String) {
candy = new_candy
print(candy)
}
Task { @MainActor in
changeCandy("Ice cream")
}
Task {
await MainActor.run {
changeCandy("Cookies")
}
}
Ice cream
Cookies
We have two primary methods to execute code on the Main Actor. The first involves using the @MainActor annotation, indicating that a task is designated to run on the Main Actor irrespective of other considerations. The second approach is to directly invoke the Main Actor using MainActor.run{}.
However, when working with SwiftUI, a more elegant approach exists, leaving no room for error in adding the @MainActor annotation or directly calling the main actor.
struct MyAwesomeView: View {
@MainActor @State private var userName = "Paolinsky"
var body: some View {
Text(userName)
}
}
By adding the @MainActor annotation to any state variable, you ensure that it will only be modified from the main thread.
Conclusion
Swift gives us many tools to build thread safe application, and now that you understand how easy and how important is altering the UI only from the main thread you do not have any excuse for not doing so.
Thanks for reading the whole post, and if you are reading this it means you found it interesting, please consider helping me keep creating these types of guides and tutorials with a small donation, and consider giving me a follow.
Top comments (0)