Actors
The café has one order book. Picture two cashiers coming up to it at the same moment. Both read the last number, 12, and both write their order down as number 13. One order is lost.
To keep that from happening, one cashier works at the book and serves one customer at a time. The other customers wait their turn, and the book stays in order.
Data race
The same thing happens in a program when two jobs run at the same time and both change one variable. A data race is two parts of a program working with one variable at the same time, with at least one of them changing it. The result depends on who got there first, and changes can be lost.
Such a bug is hard to find: a program can work correctly a hundred times and break on the hundred and first. That is why Swift 6 looks for data races while compiling. For the same reason, a function cannot change a variable from the main part of the program.
The compiler watches classes too. You cannot pass a class instance with a counter into two async let jobs. Such a program does not compile: a class does not protect its properties from changes made at the same time.
Actor
An actor is a reference type that serves only one task at a time, so two tasks never change its properties at once. It is like a class, and you declare it with the word actor instead of class:
actor OrderBook {
var count = 0
func addOrder() {
count += 1
}
}
let book = OrderBook()
await book.addOrder()
await book.addOrder()
print(await book.count) // 2Let's go through the program line by line:
actor OrderBookdeclares an actor with the propertycountand the methodaddOrder. Inside the actor, the method changescountdirectly, withoutawait.let book = OrderBook()creates an instance. An actor is a reference type, soletfixes only the reference, andcountcan still change.await book.addOrder()calls the method from outside the actor, so it needsawait: if the actor is busy with another task, the call waits its turn.print(await book.count)reads the propertycountfrom outside, also withawait, and prints2. The read waits its turn becausecountis avarproperty that calls toaddOrderchange.
Two jobs and one actor
Now let two jobs take orders at the same time, a thousand orders each:
func takeOrders(_ book: OrderBook, times: Int) async -> Int {
for _ in 1...times {
await book.addOrder()
}
return times
}
async let first = takeOrders(book, times: 1000)
async let second = takeOrders(book, times: 1000)
let taken = await first + second
print(taken, await book.count) // 2000 2002takeOrderscallsaddOrderas many times as it is asked to, each time withawait.- Two
async lettasks calltakeOrdersat the same time and write into the same book. - The actor lets the
addOrdercalls through one at a time, so no order is lost: exactly 2000 were added to the two from before.
Run the program as many times as you like: it always prints 2000 2002.
Here the cashier picture stops working. A cashier finishes one order and only then calls the next customer. But when an actor's method itself waits at await, the actor lets the next task in for that time.
Interviewers like to ask this: what is an actor, and how does calling its method differ from calling a class method? The answer:
- an actor is a reference type that serves one task at a time, so two tasks do not change its properties at once;
- you call a class method directly, but an actor's method from outside with
await: the call may have to wait its turn.
Common mistake
An actor's method was called from outside without await:
book.addOrder() // ✗error: actor-isolated instance method 'addOrder()' cannot be called from outside of the actorThe program does not compile. The message sounds as if an actor's method cannot be called from outside at all, but the problem is the missing await: the line await book.addOrder() compiles.
Key points
- A data race is two parts of a program working with one variable at the same time, with at least one changing it; changes can then be lost.
- An actor is a reference type that serves one task at a time; you declare it with the word
actor. - From outside, you call an actor's method and read its
varproperty withawait; inside the actor, directly. - Swift 6 looks for data races while compiling: a call to an actor from outside without
awaitdoes not compile.