Deep Dive • 6/17/2026
Actor Reentrancy: Solving the Suspension Gap in Swift 6
Actor Reentrancy: Solving the Suspension Gap in Swift 6
In the shift to Swift 6, many engineers mistake Actors for simple serial queues. While actors provide mutual exclusion, they are reentrant. This means that when an actor is suspended at an await, it is open for other tasks to enter. If you don’t account for this “suspension gap,” your application will suffer from subtle state corruption and logical deadlocks.
The Hook: The Reentrancy Gap
Consider a typical image cache. If two threads request the same image simultaneously, a naive actor implementation might start two separate downloads because the first one suspended, allowing the second request to enter and find the cache still empty.
The “Why”: Performance over Strict Serialization
Swift’s concurrency model favors progress. If actors weren’t reentrant, a suspended task would block the entire actor, potentially leading to widespread system stalls and deadlocks similar to those found in traditional mutex-heavy architectures. The cost of this progress is the requirement for the developer to manage atomic logical operations across suspension points.
The Implementation: Protecting State
The key is to track “in-flight” work to prevent redundant operations or inconsistent states.
actor ImageDownloader {
private enum CacheStatus {
case downloading(Task<UIImage, Error>)
case ready(UIImage)
}
private var cache: [URL: CacheStatus] = [:]
func image(for url: URL) async throws -> UIImage {
// 1. Check if we already have the image or a download in progress
if let status = cache[url] {
switch status {
case .ready(let image):
return image
case .downloading(let task):
return try await task.value
}
}
// 2. No work in progress, create a new Task
let task = Task {
try await performDownload(from: url)
}
// 3. Store the task BEFORE the first await to block other callers
cache[url] = .downloading(task)
do {
let image = try await task.value
cache[url] = .ready(image) // Update state after suspension
return image
} catch {
cache[url] = nil // Clean up on failure
throw error
}
}
}
The Verdict: Trade-offs
- Pros: Maximum system throughput; prevents actor-level deadlocks.
- Cons: Requires explicit state management; increases cognitive load.
- When to avoid: If your operations are truly synchronous and don’t require
await, standard actors or evenOSAllocatedUnfairLock(for micro-optimizations) are simpler.
Internal Connectivity
- Foundation: The Swift 6 Concurrency Manifesto
- Next Step: Beyond Sendable: Region-based Isolation