Skip to content
SwiftMind

Actors

A type that protects its own state, so data races become a compile error.

AdvancedNot started9 min
On this page

An actor is a reference type whose mutable state can only be touched from inside itself. Any access from outside has to go through an await, and the runtime guarantees only one such access runs at a time.

That's the same guarantee a lock gives you, with two differences that matter. It's enforced by the compiler rather than by everyone remembering to take the lock. And waiting for the actor suspends instead of blocking, so a contended actor doesn't tie up threads.

The result: a whole class of data race stops being a debugging problem and becomes a compile error.

Inside the actor, access is ordinary code. From outside, it's always await.
actor ScoreBoard {    private var scores: [String: Int] = [:]
    func record(_ points: Int, for player: String) {        scores[player, default: 0] += points     // inside: direct, synchronous    }
    func score(for player: String) -> Int {        scores[player] ?? 0    }
    nonisolated let boardID = UUID()             // immutable: no protection needed}
let board = ScoreBoard()await board.record(10, for: "ada")               // outside: must awaitlet total = await board.score(for: "ada")_ = board.boardID                                // nonisolated: no await
Show all 18 lines
Actor isolation
Task A
Task B
Actor queueone at a time
ScoreBoardisolated state
scoresnever raced

Two tasks both call into an actor. The actor serialises them so only one executes against its protected state at a time, while the other suspends.

Both tasks call in. Each call is an await, not a blocking wait.

Step 1 of 3
  1. Both tasks call in. Each call is an await, not a blocking wait.
  2. The actor runs one at a time. The other task suspends and its thread is reused.
  3. Isolated state is only ever touched by whichever call is currently running.

Reentrancy

Reentrancy is the sharp edge

Actors are reentrant: when an actor method suspends at an await, the actor is free to start another call. This is what stops one slow operation from deadlocking everything waiting on the actor — but it means state you checked before an await may have changed by the time you resume.

So the guarantee is narrower than "one thing at a time". It's: *your code runs uninterrupted between awaits*. Any invariant that has to hold across an await must be re-checked after it, or defended with an explicit in-progress flag.

Without the in-flight map, reentrancy would let two callers both start the same download.
actor ImageCache {    private var cache: [URL: Image] = [:]    private var inFlight: [URL: Task<Image, Error>] = [:]
    func image(at url: URL) async throws -> Image {        if let cached = cache[url] { return cached }        if let existing = inFlight[url] { return try await existing.value }  // dedupe
        let task = Task { try await download(url) }        inFlight[url] = task        defer { inFlight[url] = nil }
        let image = try await task.value        cache[url] = image        return image    }
    private func download(_ url: URL) async throws -> Image { fatalError("…") }}
Show all 19 lines

Quick check

Why does calling an actor method from outside require await?

  • Because actor methods always run on a background thread
  • Because the call may need to wait for the actor to become free, and waiting suspends rather than blocks
  • Because actors are reference types
  • Because all actor methods are implicitly async
Reveal answer

Because the call may need to wait for the actor to become free, and waiting suspends rather than blocks

The actor may already be executing another call. Rather than blocking the caller's thread, the call suspends until the actor is free. It's not about which thread the code runs on — a @MainActor method runs on the main thread and still needs await from outside. And the methods aren't implicitly async: from *inside* the actor they're called synchronously with no await at all.

Interview question

What guarantee does an actor give you, and what does it deliberately not give you?

Think first, then reveal

It guarantees exclusive access to its isolated state. Only one task executes actor code at a time, and the compiler enforces that everything outside goes through await, so you cannot accidentally touch that state concurrently. Low-level data races on those properties become impossible — that's a static guarantee, not a runtime hope.

What it doesn't give you is atomicity across suspension points. Actors are reentrant: when a method awaits, the actor can start another call. So the real guarantee is that your code runs uninterrupted *between* awaits, not that a whole method is atomic. Any invariant that spans an await has to be re-established afterwards or protected explicitly — the classic symptom is a cache that checks for a miss, awaits a fetch, and ends up fetching twice because two callers both passed the check.

Reentrancy isn't an oversight, it's the alternative to deadlock: if the actor stayed locked across an await, an actor awaiting something that eventually calls back into it would wait on itself forever.

The other non-guarantee is ordering. Callers are resumed when the actor is free, not in the order they arrived, so you can't rely on FIFO. If you need ordering, model it — a sequence number, or an AsyncStream you consume in order.

So actors solve "two threads mutating the same property". They don't solve "this multi-step operation must be atomic", and conflating the two is where people get bitten.

What an interviewer listens for

  • States the guarantee precisely: exclusive access, compiler-enforced
  • Explains reentrancy and that atomicity stops at each await
  • Justifies reentrancy as deadlock avoidance rather than calling it a flaw
  • Notes that call ordering is not guaranteed
  • Gives a concrete failure — the double-fetch cache

Key takeaway

An actor makes exclusive access to its state a compile-time guarantee instead of a convention. It is reentrant, so treat every await inside an actor as a point where your assumptions expire.

Read these first

Related

0%
Quiz this concept