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.
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
actorgives serial execution per instance;@MainActoris the main-queue actor for UI and view models. - Structured concurrency (
async let,withThrowingTaskGroup) bounds child lifetimes and propagates cancellation. NSLockandos_unfair_lockcan win micro-benchmarks but are easy to misuse; prefer actors unless you have measured contention and a clear plan.- After any
awaitinside an actor, state may have changed (reentrancy). Re-check invariants before committing writes.
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
}
}
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
}
}