Updated for Xcode 14.2
Creating and using an actor in Swift takes two steps: create the type using actor
rather than class
or struct
, then use await
when accessing its properties or methods externally. Swift takes care of everything else for us, including ensuring that properties and methods must be accessed safely.
Let’s look at a simple example: a URL cache that remembers the data for each URL it downloads. Here’s how that would be created and used:
actor URLCache {
private var cache = [URL: Data]()
func data(for url: URL) async throws -> Data {
if let cached = cache[url] {
return cached
}
let (data, _) = try await URLSession.shared.data(from: url)
cache[url] = data
return data
}
}
@main
struct App {
static func main() async throws {
let cache = URLCache()
let url = URL(string: "https://apple.com")!
let apple = try await cache.data(for: url)
let dataString = String(decoding: apple, as: UTF8.self)
print(dataString)
}
}
Download this as an Xcode project
I marked its internal cache
dictionary as private
, so the only way we can access cached data is using the data(for:)
method. This provides some degree of safety, because we might do some sort of special work inside the method that would be bypassed by accessing the property directly.
However, the real protection here is that the property and method are both encapsulated inside an actor, which means only a single thread can use them at any given time. In practice, this avoids two problems:
If we didn’t have an actor here – if we had used class URLCache
or struct URLCache
– then we would need to solve those two problems ourselves. It’s not hard, at least not in a simple way, but it’s error-prone and boring to do, so it’s great to be able to hand this work over to the Swift compiler to do for us.
However, this ease of use does come with some extra responsibility: it’s really important you keep in mind the serial queue behavior of actors, because it’s entirely possible you can create massive speed bumps in your code just because you wrote actor
rather than class
. Think about the URL cache we just made, for example – just by using actor
rather than class
when we made it, we forced it to load only a single URL at a time. If that’s what you want then you’re all set, but if not then you’ll be wondering why all its requests are handled one by one.
The canonical example of why data races are problematic – the one that is often taught in computer science degrees – is about bank accounts, because here data races can result in serious real-world problems. To see why, here’s an example BankAccount
class that handles sending and receiving money:
class BankAccount {
var balance: Decimal
init(initialBalance: Decimal) {
balance = initialBalance
}
func deposit(amount: Decimal) {
balance = balance + amount
}
func transfer(amount: Decimal, to other: BankAccount) {
// Check that we have enough money to pay
guard balance > amount else { return }
// Subtract it from our balance
balance = balance - amount
// Send it to the other account
other.deposit(amount: amount)
}
}
let firstAccount = BankAccount(initialBalance: 500)
let secondAccount = BankAccount(initialBalance: 0)
firstAccount.transfer(amount: 500, to: secondAccount)
Download this as an Xcode project
That’s a class, so Swift won’t do anything to stop us from accessing the same piece of data multiple times. So, what could actually happen here?
Well, in the worst case two parallel calls to transfer()
would be called on the same BankAccount
instance, and the following would occur:
Do you see the problem there? Well, what happens if the account we’re transferring from contains $100, and we’re asked to transfer $80 to the other account? If we follow the steps above, both calls to transfer()
will happen in parallel and see that there was enough for the transfer to take place, then both will transfer the money across. The end result is that our check for sufficient funds wasn’t useful, and one account ends up with -$60 – something that might incur fees, or perhaps not even be allowed depending on the type of account they have.
If we switch this type to be an actor, that problem goes away. This means using actor BankAccount
rather than class BankAccount
, but also using async
and await
because we can’t directly call deposit()
on the other bank account and instead need to post the request as a message to be executed later.
Here’s how that looks:
actor BankAccount {
var balance: Decimal
init(initialBalance: Decimal) {
balance = initialBalance
}
func deposit(amount: Decimal) {
balance = balance + amount
}
func transfer(amount: Decimal, to other: BankAccount) async {
// Check that we have enough money to pay
guard balance > amount else { return }
// Subtract it from our balance
balance = balance - amount
// Send it to the other account
await other.deposit(amount: amount)
}
}
let firstAccount = BankAccount(initialBalance: 500)
let secondAccount = BankAccount(initialBalance: 0)
await firstAccount.transfer(amount: 500, to: secondAccount)
Download this as an Xcode project
With that change, our bank accounts can no longer fall into negative values by accident, which avoids a potentially nasty result.
In other places, actors can prevent bizarre results that ought to be impossible. For example, what would happen if our example was a basketball team rather than a bank account, and we were transferring players rather than money? Without actors we could end up in the situation where we transfer the same player twice – Team A would end up without them, and Team B would have them twice!
SAVE 50% To celebrate WWDC23, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.
Link copied to your pasteboard.