Threading
Threading
Section titled “Threading”Use this when:
- You need to understand the relationship between tasks and threads.
- You are debugging suspension points, actor reentrancy, or unexpected execution contexts.
- You need Swift 6.2 behavior guidance (
nonisolated async,@concurrent,nonisolated(nonsending)).
Skip this file if:
- You mainly need to protect mutable state. Use
actors.md. - You need to make types safe to transfer. Use
sendable.md.
Jump to:
- Core Concepts (Tasks vs Threads)
- Cooperative Thread Pool
- Suspension Points and Actor Reentrancy
- Swift 6.2 Changes (SE-461, SE-466)
- Default Isolation Domain
- Debugging Thread Execution
- Common Misconceptions
- Migration Strategy
Core Concepts
Section titled “Core Concepts”What is a Thread?
Section titled “What is a Thread?”System-level resource that runs instructions. High overhead for creation and switching. Swift Concurrency abstracts thread management away.
Tasks vs Threads
Section titled “Tasks vs Threads”Tasks are units of async work, not tied to specific threads. Swift dynamically schedules tasks on available threads from a cooperative pool.
Key insight: No direct relationship between one task and one thread.
Important (Swift 6+): Avoid using Thread.current inside async contexts. In Swift 6 language mode, Thread.current is unavailable from asynchronous contexts and will fail to compile. Prefer reasoning in terms of isolation domains; use Instruments and the debugger to observe execution when needed.
Cooperative Thread Pool
Section titled “Cooperative Thread Pool”Swift creates only as many threads as CPU cores. Tasks share these threads efficiently.
How it works
Section titled “How it works”- Limited threads: Number matches CPU cores
- Task scheduling: Tasks scheduled onto available threads
- Suspension: At
await, task suspends, thread freed for other work - Resumption: Task resumes on any available thread (not necessarily the same one)
func example() async { print("Started on: \(Thread.current)")
try await Task.sleep(for: .seconds(1))
print("Resumed on: \(Thread.current)") // Likely different thread}Benefits over GCD
Section titled “Benefits over GCD”Prevents thread explosion:
- No excessive thread creation
- No high memory overhead from idle threads
- No excessive context switching
- No priority inversion
Better performance:
- Fewer threads = less context switching
- Continuations instead of blocking
- CPU cores stay busy efficiently
Threading Mindset → Isolation Mindset
Section titled “Threading Mindset → Isolation Mindset”Old way (GCD)
Section titled “Old way (GCD)”// Thinking about threadsDispatchQueue.main.async { // Update UI on main thread}
DispatchQueue.global(qos: .background).async { // Heavy work on background thread}New way (Swift Concurrency)
Section titled “New way (Swift Concurrency)”// Thinking about isolation domains@MainActorfunc updateUI() { // Runs on main actor (usually main thread)}
func heavyWork() async { // Runs on any available thread in pool}Think in isolation domains
Section titled “Think in isolation domains”Don’t ask: “What thread should this run on?”
Ask: “What isolation domain should own this work?”
@MainActorfor UI updates- Custom actors for specific state
- Nonisolated for general async work
Provide hints, not commands
Section titled “Provide hints, not commands”Task(priority: .userInitiated) { await doWork()}You’re describing the nature of work, not assigning threads. Swift optimizes execution.
Suspension Points
Section titled “Suspension Points”What is a suspension point?
Section titled “What is a suspension point?”Moment where task may pause to allow other work. Marked by await.
let data = await fetchData() // Potential suspensionCritical: await marks possible suspension, not guaranteed. If operation completes synchronously, no suspension occurs.
Why suspension points matter
Section titled “Why suspension points matter”- Code may pause unexpectedly - resumes later, possibly different thread
- State can change - mutable state may be modified during suspension
- Actor reentrancy - other tasks can access actor during suspension
Task.sleep follows the same rule: it suspends the task rather than blocking a thread. That still does not make actor choice irrelevant. If a delayed retry starts on @MainActor, it may wait for main-actor availability before reaching Task.sleep when scheduled from another executor or while the main actor is busy. Prefer @concurrent when the delay itself is not UI-owned, then hop back with MainActor.run for the final UI mutation.
Actor reentrancy example
Section titled “Actor reentrancy example”actor BankAccount { private var balance: Int = 0
func deposit(amount: Int) async { balance += amount print("Balance: \(balance)")
await logTransaction(amount) // ⚠️ Suspension point
balance += 10 // Bonus print("After bonus: \(balance)") }
func logTransaction(_ amount: Int) async { try? await Task.sleep(for: .seconds(1)) }}
// Two concurrent depositsasync let _ = account.deposit(amount: 100)async let _ = account.deposit(amount: 100)
// Unexpected: 100 → 200 → 210 → 220// Expected: 100 → 110 → 210 → 220Why: During logTransaction, second deposit runs, modifying balance before first completes.
Avoiding reentrancy bugs
Section titled “Avoiding reentrancy bugs”Complete actor work before suspending:
func deposit(amount: Int) async { balance += amount balance += 10 // Bonus applied first print("Final balance: \(balance)")
await logTransaction(amount) // Suspend after state changes}Rule: Don’t mutate actor state after suspension points.
Thread Execution Patterns
Section titled “Thread Execution Patterns”Default: Background threads
Section titled “Default: Background threads”Tasks run on cooperative thread pool (background threads):
Task { print(Thread.current) // Background thread}Main thread execution
Section titled “Main thread execution”Use @MainActor for main thread:
@MainActorfunc updateUI() { Task { print(Thread.current) // Main thread }}Inheritance example
Section titled “Inheritance example”@MainActorfunc updateUI() { print("Main thread: \(Thread.current)")
await backgroundTask() // Switches to background
print("Back on main: \(Thread.current)") // Returns to main}
func backgroundTask() async { print("Background: \(Thread.current)")}Swift 6.2 Changes
Section titled “Swift 6.2 Changes”Nonisolated async functions (SE-461)
Section titled “Nonisolated async functions (SE-461)”Old behavior: Nonisolated async functions always switch to background.
New behavior: Inherit caller’s isolation by default.
class NotSendable { func performAsync() async { print(Thread.current) }}
@MainActorfunc caller() async { let obj = NotSendable() await obj.performAsync() // Old: Background thread // New: Main thread (inherits @MainActor)}Enabling new behavior
Section titled “Enabling new behavior”In Xcode 16+:
// Build setting or swift-settings.enableUpcomingFeature("NonisolatedNonsendingByDefault")Opting out with @concurrent
Section titled “Opting out with @concurrent”Force function to switch away from caller’s isolation:
@concurrentfunc performAsync() async { print(Thread.current) // Always background}nonisolated(nonsending)
Section titled “nonisolated(nonsending)”Prevent sending non-Sendable values across isolation:
nonisolated(nonsending) func storeTouch(...) async { // Runs on caller's isolation, no value sending}Use when: Method doesn’t need to switch isolation, avoiding Sendable requirements.
Default Isolation Domain (SE-466)
Section titled “Default Isolation Domain (SE-466)”Configuring default isolation
Section titled “Configuring default isolation”Build setting (Xcode 16+):
- Default Actor Isolation:
MainActororNone
Swift Package:
.target( name: "MyTarget", swiftSettings: [ .defaultIsolation(MainActor.self) ])Why change default?
Section titled “Why change default?”Most app code runs on main thread. Setting @MainActor as default:
- Reduces false warnings
- Avoids “concurrency rabbit hole”
- Makes migration easier
Inference with @MainActor default
Section titled “Inference with @MainActor default”// With @MainActor as default:
func f() {} // Inferred: @MainActor
class C { init() {} // Inferred: @MainActor static var value = 10 // Inferred: @MainActor}
@MyActorstruct S { func f() {} // Inferred: @MyActor (explicit override)}Per-module setting
Section titled “Per-module setting”Must opt in for each module/package. Not global across dependencies.
Backward compatibility
Section titled “Backward compatibility”Opt-in only. Default remains nonisolated if not specified.
Debugging Thread Execution
Section titled “Debugging Thread Execution”Print current thread
Section titled “Print current thread”⚠️ Important: Thread.current is unavailable in Swift 6 language mode from async contexts. The compiler error states: “Class property ‘current’ is unavailable from asynchronous contexts; Thread.current cannot be used from async contexts.”
Workaround (Swift 6+ mode only):
extension Thread { public static var currentThread: Thread { Thread.current }}
print("Thread: \(Thread.currentThread)")Debug navigator
Section titled “Debug navigator”- Set breakpoint in task
- Debug → Pause
- Check Debug Navigator for thread info
Verify main thread
Section titled “Verify main thread”assert(Thread.isMainThread)Common Misconceptions
Section titled “Common Misconceptions”❌ Each Task runs on new thread
Section titled “❌ Each Task runs on new thread”Wrong. Tasks share limited thread pool, reuse threads.
❌ await blocks the thread
Section titled “❌ await blocks the thread”Wrong. await suspends task without blocking thread. Other tasks can use the thread.
❌ Task execution order is guaranteed
Section titled “❌ Task execution order is guaranteed”Wrong. Tasks execute based on system scheduling. Use await to enforce order.
❌ Same task = same thread
Section titled “❌ Same task = same thread”Wrong. Task can resume on different thread after suspension.
Why Sendable Matters
Section titled “Why Sendable Matters”Since tasks move between threads unpredictably:
func example() async { print("Thread 1: \(Thread.current)")
await someWork()
print("Thread 2: \(Thread.current)") // Different thread}Values crossing suspension points may cross threads. Sendable ensures safety.
Best Practices
Section titled “Best Practices”- Stop thinking about threads - think isolation domains
- Trust the system - Swift optimizes thread usage
- Use @MainActor for UI - clear, explicit main thread execution
- Minimize suspension points in actors - avoid reentrancy bugs
- Complete state changes before suspending - prevent inconsistent state
- Use priorities as hints - not guarantees
- Make types Sendable - safe across thread boundaries
- Enable Swift 6.2 features - easier migration, better defaults
- Set default isolation for apps - reduce false warnings
- Don’t force thread switching - let Swift optimize
Migration Strategy
Section titled “Migration Strategy”For new projects (Xcode 16+)
Section titled “For new projects (Xcode 16+)”- Set default isolation to
@MainActor - Enable
NonisolatedNonsendingByDefault - Use
@concurrentfor explicit background work
For existing projects
Section titled “For existing projects”- Gradually enable Swift 6 language mode
- Consider default isolation change
- Use
@concurrentto maintain old behavior where needed - Migrate module by module
Decision Tree
Section titled “Decision Tree”Need to control execution?├─ UI updates? → @MainActor├─ Specific state isolation? → Custom actor├─ Background work? → Regular async (trust Swift)└─ Need to force background? → @concurrent (Swift 6.2+)
Seeing Sendable warnings?├─ Can make type Sendable? → Add conformance├─ Same isolation OK? → nonisolated(nonsending)└─ Need different isolation? → Make Sendable or refactorGCD to Isolation Domain Migration
Section titled “GCD to Isolation Domain Migration”Instead of asking “what thread should this run on?” ask “what isolation domain should own this work?”
DispatchQueue.main.async { }→@MainActor func updateUI()DispatchQueue.global().async { }→func work() async(or@concurrentif it must leave caller isolation)DispatchQueue(label:).sync { }→actororMutexfor protecting state- Serial queue for ordering →
actor(guarantees serial access)
Decision Rules
Section titled “Decision Rules”- UI state → usually
@MainActor - Mutable shared state → usually an
actor - Plain async work with no isolated state →
asyncAPI with explicit ownership - Work that must hop away from caller isolation under Swift 6.2-era behavior → consider
@concurrent
Common Mistakes Agents Make
Section titled “Common Mistakes Agents Make”- Recommending GCD queue hopping when actor isolation already expresses the ownership model.
- Debugging correctness by thread ID instead of by isolation and ordering.
- Treating
awaitas a blocking call — it suspends the task, freeing the thread. - Mapping each
Taskto a conceptual thread.
Performance Insights
Section titled “Performance Insights”Why fewer threads = better performance
Section titled “Why fewer threads = better performance”- Less context switching: CPU spends more time on actual work
- Better cache utilization: Threads stay on same cores longer
- No thread explosion: Predictable resource usage
- Forward progress: Threads never block, always productive
Cooperative pool advantages
Section titled “Cooperative pool advantages”- Matches hardware (one thread per core)
- Prevents oversubscription
- Efficient task scheduling
- Automatic load balancing