Skip to content
Theory · Asynchronous code · lesson 6 of 7

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:

SWIFT
actor OrderBook {
    var count = 0

    func addOrder() {
        count += 1
    }
}

let book = OrderBook()
await book.addOrder()
await book.addOrder()
print(await book.count) // 2

Let's go through the program line by line:

  1. actor OrderBook declares an actor with the property count and the method addOrder. Inside the actor, the method changes count directly, without await.
  2. let book = OrderBook() creates an instance. An actor is a reference type, so let fixes only the reference, and count can still change.
  3. await book.addOrder() calls the method from outside the actor, so it needs await: if the actor is busy with another task, the call waits its turn.
  4. print(await book.count) reads the property count from outside, also with await, and prints 2. The read waits its turn because count is a var property that calls to addOrder change.

Two jobs and one actor

Now let two jobs take orders at the same time, a thousand orders each:

SWIFT
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 2002
  1. takeOrders calls addOrder as many times as it is asked to, each time with await.
  2. Two async let tasks call takeOrders at the same time and write into the same book.
  3. The actor lets the addOrder calls 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:

SWIFT
book.addOrder() // ✗
TEXT
error: actor-isolated instance method 'addOrder()' cannot be called from outside of the actor

The 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 var property with await; inside the actor, directly.
  • Swift 6 looks for data races while compiling: a call to an actor from outside without await does not compile.