What is an actor in Swift, and what is actor reentrancy?
TL;DR
An actor is like a class, but it protects its own state from data races. Only one task can run code on the actor at a time.
From outside, you must await to access it. The compiler checks this.
Actors are reentrant: while a method is paused at await, other calls can run. So state can change across an await.
A simple actor
actor BankAccount {
private(set) var balance = 0
func deposit(_ amount: Int) {
balance += amount // safe: only one task at a time
}
func withdraw(_ amount: Int) -> Bool {
guard balance >= amount else { return false }
balance -= amount
return true
}
}
let account = BankAccount()
await account.deposit(100)
let ok = await account.withdraw(30)
print(await account.balance) // 70Inside the actor, no await is needed for its own state. Outside, every access needs await, because you may need to wait for your turn.
Why not a class with a lock?
| Class + lock / queue | Actor | |
|---|---|---|
| Safety | You must remember to lock | Checked by the compiler |
| Waiting | Blocks a thread | Suspends, thread is free |
| Deadlock risk | Yes, with nested locks | No lock-based deadlocks |
| Works with async code | Awkward | Natural |
Actor isolation rules
- Mutable state of an actor is isolated. Only the actor's own code can touch it directly.
- Values passed in or out must be
Sendable(safe to share). - Actors are reference types, and they are always
Sendable.
actor Cache {
private var store: [String: Data] = [:]
func get(_ key: String) -> Data? { store[key] }
}
let cache = Cache()
// cache.store["a"] = nil // error: actor-isolated property
let d = await cache.get("a") // OKnonisolated members
Code that does not touch mutable state can opt out of isolation. Then you call it without await.
actor User {
let id: UUID // let constants of Sendable type are safe
var name = ""
init(id: UUID) { self.id = id }
nonisolated var debugID: String { id.uuidString }
}Reentrancy: the classic trap
When an actor method hits await, the actor is free for other calls. When the method resumes, the state may be different.
actor ImageCache {
private var images: [URL: Data] = [:]
func image(for url: URL) async throws -> Data {
if let cached = images[url] { return cached }
let data = try await download(url) // actor is free here!
images[url] = data // another call may have set it already
return data
}
}If two callers ask for the same URL at once, both miss the cache and both download. Not a crash, but wasted work.
Fix: store the in-flight task
actor ImageCache {
private var tasks: [URL: Task<Data, Error>] = [:]
func image(for url: URL) async throws -> Data {
if let task = tasks[url] { return try await task.value }
let task = Task { try await download(url) }
tasks[url] = task // set before the await
return try await task.value
}
}Now the second caller finds the task and awaits the same result.
Rule: do not assume state is unchanged after an await. Check it again, or update state before you suspend.
Why are actors reentrant?
If an actor waited for each method to fully finish, two actors calling each other could deadlock. Reentrancy avoids that. The cost is that you must think about state across await.
Global actors
A global actor is one shared actor that many types and functions can use. @MainActor is the most famous one.
@globalActor
actor DatabaseActor {
static let shared = DatabaseActor()
}
@DatabaseActor
final class Database {
var rows: [String] = []
}Common follow-up questions
- Do actors run on a specific thread? No (except
MainActor). They run on the shared pool. Swift only guarantees one task at a time. - Can actors inherit? No. Actors do not support inheritance.
- Are actor calls in order? Not strictly. They are not guaranteed to run in the order they were called. For strict ordering, use an
AsyncStreamas a queue. - When should you not use an actor? For UI state (use
@MainActor), or for very hot, tiny critical sections where aMutexis cheaper. - What is an
isolatedparameter?func run(on actor: isolated MyActor)runs the function on that actor, so it can access its state withoutawait.