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
Race conditions come from shared mutable state, so the design goal is to remove it or put it behind one controlled access point. Prefer immutable data where you can. Where mutation is unavoidable, confine it to one serial executor so only one task writes at a time.
Model the shared resource as an actor, or mark UI-facing types @MainActor, so the compiler enforces mutual exclusion rather than relying on discipline.
Only one task runs actor-isolated code at a time, so reads and writes cannot race. The cost is that every cross-actor call needs await, and after any await inside the actor another task may have changed the state. That's reentrancy, so re-check the state before you write.
Start with actors for safety. If one actor becomes a bottleneck because many tasks queue on it, split the state into smaller actors with narrower responsibilities.
Principles
- A data race is unsynchronized concurrent access where at least one side writes, and Swift 6 catches many at compile time.
- An
actorserializes access to its own state, and@MainActoris the actor for UI and view models. - Structured concurrency with
async letand task groups bounds child lifetimes and propagates cancellation. - Locks such as
NSLockcan win micro-benchmarks but are easy to misuse, so prefer actors unless you have measured contention. - Reentrancy means another task can change actor state while you're suspended, so re-check invariants after every
await.
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)
// Re-check after the suspension point: another task may have filled the entry
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()
return 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
}
}



