Chapter 9 — Actors: taming shared mutable state
The bug an actor prevents
// DANGER: a plain class touched from multiple tasks races on `storage`.
final class UnsafeImageCache {
private var storage: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? { storage[url] }
func insert(_ image: UIImage, for url: URL) { storage[url] = image } // ⚠️ concurrent writes race
}
flowchart TB
T1["task 1: insert(A)"] --> S["storage dictionary
(one buffer)"] T2["task 2: insert(B)"] --> S T3["task 3: insert(C)"] --> S S --> CORRUPT["simultaneous writes →
corruption / crash 💥"]
(one buffer)"] T2["task 2: insert(B)"] --> S T3["task 3: insert(C)"] --> S S --> CORRUPT["simultaneous writes →
corruption / crash 💥"]
The actor: state that only one task touches at a time
// ImageCache/ImageCache.swift
actor ImageCache {
private var storage: [URL: UIImage] = [:]
func image(for url: URL) -> UIImage? { storage[url] }
func insert(_ image: UIImage, for url: URL) { storage[url] = image }
}
An actor serializes all access to its mutable state. One task at a time, always. Data races on that state become impossible — and the compiler enforces it.
Reaching an actor from outside is async
let cache = ImageCache()
// From outside the actor, access is async — you await, because you might have to wait your turn.
await cache.insert(image, for: url)
let cached = await cache.image(for: url)
flowchart LR
A["caller task"] -->|"await cache.insert(...)"| HOP["hop onto cache's
serial executor"] HOP --> RUN["run insert
(exclusive access)"] RUN --> BACK["hop back to caller"]
serial executor"] HOP --> RUN["run insert
(exclusive access)"] RUN --> BACK["hop back to caller"]
actor ImageCache {
private var storage: [URL: UIImage] = [:]
private var hitCount = 0
func image(for url: URL) -> UIImage? {
if storage[url] != nil { hitCount += 1 } // no await — we're already isolated here
return storage[url]
}
}
nonisolated: opting out for state-free members
actor ImageCache {
let name: String // immutable — safe to read from anywhere
private var storage: [URL: UIImage] = [:]
init(name: String) { self.name = name }
// Touches only `name` (immutable) — no isolation needed, so no await for callers.
nonisolated var description: String { "ImageCache(\(name))" }
}
let cache = ImageCache(name: "avatars")
print(cache.description) // no await — nonisolated
print(cache.name) // no await — `let` constants are implicitly nonisolated
A complete, useful ImageCache
// ImageCache/ImageCache.swift
actor ImageCache {
private var storage: [URL: UIImage] = [:]
private let client: APIClient
init(client: APIClient = APIClient()) { self.client = client }
func image(for url: URL) async throws -> UIImage {
// Fast path: cache hit. Synchronous access to our own state.
if let cached = storage[url] { return cached }
// Miss: download it. Note the `await` — the actor SUSPENDS here (more on
// the consequences of that in Chapter 10).
let (data, _) = try await client.data(from: url)
guard let image = UIImage(data: data) else { throw FetchError.badImage }
storage[url] = image // back on the actor; safe synchronous write
return image
}
}
let avatar = try await imageCache.image(for: avatarURL)
Actors are Sendable; their isolated state is not
What we built in this chapter
- ImageCache, a race-free image cache built as an
actor— the sameDictionarythat corrupted - The visible rule: accessing an actor from outside is
async(youawait, because you might wait nonisolatedmembers for immutable data and pure computation, which the compiler forbids from- The observation that an
awaitinside an actor method suspends the actor — setting up Chapter 10's - That actors are
Sendable(safe to share) precisely because their state stays protected inside
Mental model to take away
- An actor serializes access to its mutable state — one task at a time — so data races on that state
- The
awaitat an actor call is the isolation boundary: outside → async (you may hop and wait); - Changing
classtoactoris the smallest possible change with the largest safety payoff — but it - An actor protects state that stays inside it; what crosses the boundary is governed by