← All topics/Concurrency

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

Question 1

Race conditions in structured concurrency

You have multiple async tasks mutating shared state. How would you design the system to prevent race conditions?

Answer outline

In concurrent code, shared mutable state is where race conditions usually come from, so avoid it or isolate it behind one controlled access point. Prefer immutable data where you can, or confine all mutations to a single serial executor so only one task mutates at a time.

Model the shared resource as an actor (or mark UI-facing types @MainActor) so the compiler enforces mutual exclusion.

Think of an actor as a protected room for its own state: only one task can be inside actor-isolated code at a time, so reads and writes do not race. The tradeoff is that every cross-actor call needs await, and one giant actor can become a bottleneck.

Start with actors for safety. If one actor becomes a bottleneck because many tasks are waiting on it, split the state into smaller actors.

Actor serialization model — one task inside at a time, reentrancy after await

Principles

  • A data race is unsynchronized concurrent access where at least one access is a write. Swift now surfaces many of these at compile time.
  • An actor gives serial execution per instance; @MainActor is the main-queue actor for UI and view models.
  • Structured concurrency (async let, withThrowingTaskGroup) bounds child lifetimes and propagates cancellation.
  • NSLock and os_unfair_lock can win micro-benchmarks but are easy to misuse; prefer actors unless you have measured contention and a clear plan.
  • After any await inside an actor, state may have changed (reentrancy). Re-check invariants before committing writes.
actor — shared cache, compiler-enforced serialization
actor ImageCache {
    private var store: [URL: Data] = [:]

    func image(for url: URL) async throws -> Data {
        if let hit = store[url] { return hit }
        // Suspension point: another task may enter the actor before we resume
        let data = try await download(url)

        // Verify the state again after the suspension point. 
        if let existing = store[url] { return existing }
        store[url] = data
        return data
    }

    private func download(_ url: URL) async throws -> Data {
        let (data, _) = try await URLSession.shared.data(from: url)
        return data
    }
}
Pair with structured concurrency at call site
func loadThumbnails(urls: [URL]) async throws -> [URL: Data] {
    let cache = ImageCache()
    try await withThrowingTaskGroup(of: (URL, Data).self) { group in
        for url in urls {
            group.addTask {
                let data = try await cache.image(for: url)
                return (url, data)
            }
        }
        var result: [URL: Data] = [:]
        for try await (url, data) in group {
            result[url] = data
        }
        return result
    }
}