Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Diagnose and fix Swift Concurrency issues: async/await, actor isolation, Sendable, and Swift 6 migration.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/threading.md
1# Threading23Use this when:45- You need to understand the relationship between tasks and threads.6- You are debugging suspension points, actor reentrancy, or unexpected execution contexts.7- You need Swift 6.2 behavior guidance (`nonisolated async`, `@concurrent`, `nonisolated(nonsending)`).89Skip this file if:1011- You mainly need to protect mutable state. Use `actors.md`.12- You need to make types safe to transfer. Use `sendable.md`.1314Jump to:1516- Core Concepts (Tasks vs Threads)17- Cooperative Thread Pool18- Suspension Points and Actor Reentrancy19- Swift 6.2 Changes (SE-461, SE-466)20- Default Isolation Domain21- Debugging Thread Execution22- Common Misconceptions23- Migration Strategy2425## Core Concepts2627### What is a Thread?2829System-level resource that runs instructions. High overhead for creation and switching. Swift Concurrency abstracts thread management away.3031### Tasks vs Threads3233**Tasks** are units of async work, not tied to specific threads. Swift dynamically schedules tasks on available threads from a cooperative pool.3435**Key insight**: No direct relationship between one task and one thread.3637> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.1: How Threads relate to Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)3839**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.4041## Cooperative Thread Pool4243Swift creates only as many threads as CPU cores. Tasks share these threads efficiently.4445### How it works46471. **Limited threads**: Number matches CPU cores482. **Task scheduling**: Tasks scheduled onto available threads493. **Suspension**: At `await`, task suspends, thread freed for other work504. **Resumption**: Task resumes on any available thread (not necessarily the same one)5152```swift53func example() async {54print("Started on: \(Thread.current)")5556try await Task.sleep(for: .seconds(1))5758print("Resumed on: \(Thread.current)") // Likely different thread59}60```6162### Benefits over GCD6364**Prevents thread explosion**:65- No excessive thread creation66- No high memory overhead from idle threads67- No excessive context switching68- No priority inversion6970**Better performance**:71- Fewer threads = less context switching72- Continuations instead of blocking73- CPU cores stay busy efficiently7475## Threading Mindset → Isolation Mindset7677### Old way (GCD)7879```swift80// Thinking about threads81DispatchQueue.main.async {82// Update UI on main thread83}8485DispatchQueue.global(qos: .background).async {86// Heavy work on background thread87}88```8990### New way (Swift Concurrency)9192```swift93// Thinking about isolation domains94@MainActor95func updateUI() {96// Runs on main actor (usually main thread)97}9899func heavyWork() async {100// Runs on any available thread in pool101}102```103104### Think in isolation domains105106**Don't ask**: "What thread should this run on?"107108**Ask**: "What isolation domain should own this work?"109110- `@MainActor` for UI updates111- Custom actors for specific state112- Nonisolated for general async work113114### Provide hints, not commands115116```swift117Task(priority: .userInitiated) {118await doWork()119}120```121122You're describing the nature of work, not assigning threads. Swift optimizes execution.123124> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.2: Getting rid of the "Threading Mindset"](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)125126## Suspension Points127128### What is a suspension point?129130Moment where task **may** pause to allow other work. Marked by `await`.131132```swift133let data = await fetchData() // Potential suspension134```135136**Critical**: `await` marks *possible* suspension, not guaranteed. If operation completes synchronously, no suspension occurs.137138### Why suspension points matter1391401. **Code may pause unexpectedly** - resumes later, possibly different thread1412. **State can change** - mutable state may be modified during suspension1423. **Actor reentrancy** - other tasks can access actor during suspension143144The same entry-isolation rule applies to any unstructured task: choose startup isolation by what the synchronous prefix needs. If nothing before the first `await` needs the main actor—whether that first operation is `Task.sleep`, an actor hop, a `print`, or a Sendable computation—prefer `Task { @concurrent in ... }` and hop back with `MainActor.run` only for the UI mutation. If the synchronous prefix already needs main actor for one statement, keep nearby cheap lines on main with it instead of splitting them out.145146### Actor reentrancy example147148```swift149actor BankAccount {150private var balance: Int = 0151152func deposit(amount: Int) async {153balance += amount154print("Balance: \(balance)")155156await logTransaction(amount) // ⚠️ Suspension point157158balance += 10 // Bonus159print("After bonus: \(balance)")160}161162func logTransaction(_ amount: Int) async {163try? await Task.sleep(for: .seconds(1))164}165}166167// Two concurrent deposits168async let _ = account.deposit(amount: 100)169async let _ = account.deposit(amount: 100)170171// Unexpected: 100 → 200 → 210 → 220172// Expected: 100 → 110 → 210 → 220173```174175**Why**: During `logTransaction`, second deposit runs, modifying balance before first completes.176177### Avoiding reentrancy bugs178179**Complete actor work before suspending**:180181```swift182func deposit(amount: Int) async {183balance += amount184balance += 10 // Bonus applied first185print("Final balance: \(balance)")186187await logTransaction(amount) // Suspend after state changes188}189```190191**Rule**: Don't mutate actor state after suspension points.192193> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.3: Understanding Task suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)194195196## Choosing Task entry isolation197198For unstructured `Task { ... }`, choose entry isolation based on the synchronous prefix (everything before the first `await`), not on where the task was created.199200Two common reasons a bare `Task { ... }` starts on `@MainActor`:201- The task is spawned from a `@MainActor` context.202- The module enables default main-actor isolation (for example, `defaultIsolation(MainActor.self)`).203204Rule:205- If the synchronous prefix contains any main-actor work, keep inherited main-actor entry.206- If the synchronous prefix contains no main-actor work, start with `Task { @concurrent in ... }` and hop back to `MainActor` only when needed.207208```swift209// ❌ Synchronous prefix is empty; first work hops away210Task {211await hopToOtherIsolationDomain()212}213214// ❌ Synchronous prefix is only `print` (trivial, non-main); first await hops away215Task {216print("Also not main-thread-bound")217await hopToOtherIsolationDomain()218}219220// ✅ Start off the main actor, hop back only for UI work221Task { @concurrent in222await hopToOtherIsolationDomain()223await MainActor.run { updateUI() }224}225226// ✅ Synchronous prefix DOES contain main-actor work — keep inheritance227Task {228print("debug") // trivial, non-main — rides along229self.isLoading = true // needs @MainActor, before any await230await fetchData()231}232```233234The delayed-retry `Task.sleep` pattern (see `performance.md` "Match Task entry isolation to its synchronous prefix") is a specialization of this same rule: the wait is usually not UI-owned, while the final mutation is.235236Note that `Task { @concurrent in ... }` changes the closure's isolation, so any capture of non-Sendable state from the enclosing actor must move inside the `MainActor.run { ... }` hop, or be captured weakly (e.g., `[weak self]` plus a `guard let self`) before being used there. The examples above stay safe by keeping `self` use inside `MainActor.run`. If the body needs to touch non-Sendable state directly, see `sendable.md` before reaching for `@concurrent`.237238## Thread Execution Patterns239240### Default: Background threads241242Tasks run on cooperative thread pool (background threads):243244```swift245Task {246print(Thread.current) // Background thread247}248```249250### Main thread execution251252Use `@MainActor` for main thread:253254```swift255@MainActor256func updateUI() {257Task {258print(Thread.current) // Main thread259}260}261```262263### Inheritance example264265```swift266@MainActor267func updateUI() {268print("Main thread: \(Thread.current)")269270await backgroundTask() // Switches to background271272print("Back on main: \(Thread.current)") // Returns to main273}274275func backgroundTask() async {276print("Background: \(Thread.current)")277}278```279280## Swift 6.2 Changes281282### Nonisolated async functions (SE-461)283284**Old behavior**: Nonisolated async functions always switch to background.285286**New behavior**: Inherit caller's isolation by default.287288```swift289class NotSendable {290func performAsync() async {291print(Thread.current)292}293}294295@MainActor296func caller() async {297let obj = NotSendable()298await obj.performAsync()299// Old: Background thread300// New: Main thread (inherits @MainActor)301}302```303304### Enabling new behavior305306In Xcode 16+:307308```swift309// Build setting or swift-settings310.enableUpcomingFeature("NonisolatedNonsendingByDefault")311```312313### Opting out with @concurrent314315Force function to switch away from caller's isolation:316317```swift318@concurrent319func performAsync() async {320print(Thread.current) // Always background321}322```323324### nonisolated(nonsending)325326Prevent sending non-Sendable values across isolation:327328```swift329nonisolated(nonsending) func storeTouch(...) async {330// Runs on caller's isolation, no value sending331}332```333334> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.4: Dispatching to different threads using nonisolated(nonsending) and @concurrent (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)335336**Use when**: Method doesn't need to switch isolation, avoiding Sendable requirements.337338## Default Isolation Domain (SE-466)339340### Configuring default isolation341342**Build setting** (Xcode 16+):343- Default Actor Isolation: `MainActor` or `None`344345**Swift Package**:346347```swift348.target(349name: "MyTarget",350swiftSettings: [351.defaultIsolation(MainActor.self)352]353)354```355356### Why change default?357358Most app code runs on main thread. Setting `@MainActor` as default:359- Reduces false warnings360- Avoids "concurrency rabbit hole"361- Makes migration easier362363### Inference with @MainActor default364365```swift366// With @MainActor as default:367368func f() {} // Inferred: @MainActor369370class C {371init() {} // Inferred: @MainActor372static var value = 10 // Inferred: @MainActor373}374375@MyActor376struct S {377func f() {} // Inferred: @MyActor (explicit override)378}379380> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.5: Controlling the default isolation domain (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)381```382383### Per-module setting384385Must opt in for each module/package. Not global across dependencies.386387### Backward compatibility388389Opt-in only. Default remains `nonisolated` if not specified.390391## Debugging Thread Execution392393### Print current thread394395**⚠️ 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."396397**Workaround** (Swift 6+ mode only):398399```swift400extension Thread {401public static var currentThread: Thread {402Thread.current403}404}405406print("Thread: \(Thread.currentThread)")407```408409### Debug navigator4104111. Set breakpoint in task4122. Debug → Pause4133. Check Debug Navigator for thread info414415### Verify main thread416417```swift418assert(Thread.isMainThread)419```420421## Common Misconceptions422423### ❌ Each Task runs on new thread424425**Wrong**. Tasks share limited thread pool, reuse threads.426427### ❌ await blocks the thread428429**Wrong**. `await` suspends task without blocking thread. Other tasks can use the thread.430431### ❌ Task execution order is guaranteed432433**Wrong**. Tasks execute based on system scheduling. Use `await` to enforce order.434435### ❌ Same task = same thread436437**Wrong**. Task can resume on different thread after suspension.438439## Why Sendable Matters440441Since tasks move between threads unpredictably:442443```swift444func example() async {445print("Thread 1: \(Thread.current)")446447await someWork()448449print("Thread 2: \(Thread.current)") // Different thread450}451```452453Values crossing suspension points may cross threads. **Sendable** ensures safety.454455## Best Practices4564571. **Stop thinking about threads** - think isolation domains4582. **Trust the system** - Swift optimizes thread usage4593. **Use @MainActor for UI** - clear, explicit main thread execution4604. **Minimize suspension points in actors** - avoid reentrancy bugs4615. **Complete state changes before suspending** - prevent inconsistent state4626. **Use priorities as hints** - not guarantees4637. **Make types Sendable** - safe across thread boundaries4648. **Enable Swift 6.2 features** - easier migration, better defaults4659. **Set default isolation for apps** - reduce false warnings46610. **Don't force thread switching** - let Swift optimize467468## Migration Strategy469470### For new projects (Xcode 16+)4714721. Set default isolation to `@MainActor`4732. Enable `NonisolatedNonsendingByDefault`4743. Use `@concurrent` for explicit background work475476### For existing projects4774781. Gradually enable Swift 6 language mode4792. Consider default isolation change4803. Use `@concurrent` to maintain old behavior where needed4814. Migrate module by module482483## Decision Tree484485```486Need to control execution?487├─ UI updates? → @MainActor488├─ Specific state isolation? → Custom actor489├─ Background work? → Regular async (trust Swift)490└─ Need to force background? → @concurrent (Swift 6.2+)491492Seeing Sendable warnings?493├─ Can make type Sendable? → Add conformance494├─ Same isolation OK? → nonisolated(nonsending)495└─ Need different isolation? → Make Sendable or refactor496```497498## GCD to Isolation Domain Migration499500Instead of asking "what thread should this run on?" ask "what isolation domain should own this work?"501502- `DispatchQueue.main.async { }` → `@MainActor func updateUI()`503- `DispatchQueue.global().async { }` → `func work() async` (or `@concurrent` if it must leave caller isolation)504- `DispatchQueue(label:).sync { }` → `actor` or `Mutex` for protecting state505- Serial queue for ordering → `actor` (guarantees serial access)506507## Decision Rules508509- UI state → usually `@MainActor`510- Mutable shared state → usually an `actor`511- Plain async work with no isolated state → `async` API with explicit ownership512- Work that must hop away from caller isolation under Swift 6.2-era behavior → consider `@concurrent`513514## Common Mistakes Agents Make515516- Recommending GCD queue hopping when actor isolation already expresses the ownership model.517- Debugging correctness by thread ID instead of by isolation and ordering.518- Treating `await` as a blocking call — it suspends the task, freeing the thread.519- Mapping each `Task` to a conceptual thread.520- Picking task entry isolation by the enclosing context instead of by the task's synchronous prefix. A `Task { ... }` from `@MainActor` whose first `await` immediately hops away (with no main-actor work before it) should usually be `Task { @concurrent in ... }`.521522## Performance Insights523524### Why fewer threads = better performance525526- **Less context switching**: CPU spends more time on actual work527- **Better cache utilization**: Threads stay on same cores longer528- **No thread explosion**: Predictable resource usage529- **Forward progress**: Threads never block, always productive530531### Cooperative pool advantages532533- Matches hardware (one thread per core)534- Prevents oversubscription535- Efficient task scheduling536- Automatic load balancing537538## Further Learning539540For migration strategies, real-world examples, and advanced threading patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).541542