← All shorts
55Swift 5.5+2 min read

Actors: Data Race Protection by Construction

Actors serialize access to their mutable state so the compiler, not a lock or a queue, prevents data races.

A plain class shared across queues needs manual synchronization — a serial DispatchQueue, an NSLock, or os_unfair_lock — and forgetting to wrap even one access to a mutable property is a data race the compiler won't catch until it crashes in production.

#The old way

final class ImageCache {
    private let queue = DispatchQueue(label: "com.app.imagecache")
    private var storage: [String: UIImage] = [:]

    func image(for key: String) -> UIImage? {
        queue.sync { storage[key] }
    }

    func insert(_ image: UIImage, for key: String) {
        queue.sync { storage[key] = image }
    }
}

#The new way

actor ImageCache {
    private var storage: [String: UIImage] = [:]

    func image(for key: String) -> UIImage? {
        storage[key]
    }

    func insert(_ image: UIImage, for key: String) {
        storage[key] = image
    }
}

func loadThumbnail(key: String, cache: ImageCache) async -> UIImage? {
    if let cached = await cache.image(for: key) {
        return cached
    }
    let image = UIImage()
    await cache.insert(image, for: key)
    return image
}

#Why it matters

  • The compiler enforces the await at every call site that touches actor state from outside — you cannot forget the synchronization boundary
  • No manual queue or lock to create, name, and remember to use consistently
  • Actor state is isolated by construction; there is no "storage" accessible without going through the actor's interface
  • Methods called from inside the actor itself run synchronously against its own state — no await needed for self

#Gotcha

Actors are reentrant: when a method suspends at an await, other calls into the same actor can run before the first one resumes, and the actor's state can change out from under you across that suspension point. Code that reads a value, awaits something, then acts on that value based on an assumption it's still true needs to re-check state after the await, not just before it.

swiftconcurrency

Related shorts