.agents/skills/swift-concurrency/references/threading.md
Use this when:
nonisolated async, @concurrent, nonisolated(nonsending)).Skip this file if:
actors.md.sendable.md.Jump to:
System-level resource that runs instructions. High overhead for creation and switching. Swift Concurrency abstracts thread management away.
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.
Course Deep Dive: This topic is covered in detail in Lesson 7.1: How Threads relate to Tasks
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.
Swift creates only as many threads as CPU cores. Tasks share these threads efficiently.
await, task suspends, thread freed for other workfunc example() async {
print("Started on: \(Thread.current)")
try await Task.sleep(for: .seconds(1))
print("Resumed on: \(Thread.current)") // Likely different thread
}
Prevents thread explosion:
Better performance:
// Thinking about threads
DispatchQueue.main.async {
// Update UI on main thread
}
DispatchQueue.global(qos: .background).async {
// Heavy work on background thread
}
// Thinking about isolation domains
@MainActor
func updateUI() {
// Runs on main actor (usually main thread)
}
func heavyWork() async {
// Runs on any available thread in pool
}
Don't ask: "What thread should this run on?"
Ask: "What isolation domain should own this work?"
@MainActor for UI updatesTask(priority: .userInitiated) {
await doWork()
}
You're describing the nature of work, not assigning threads. Swift optimizes execution.
Course Deep Dive: This topic is covered in detail in Lesson 7.2: Getting rid of the "Threading Mindset"
Moment where task may pause to allow other work. Marked by await.
let data = await fetchData() // Potential suspension
Critical: await marks possible suspension, not guaranteed. If operation completes synchronously, no suspension occurs.
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 deposits
async let _ = account.deposit(amount: 100)
async let _ = account.deposit(amount: 100)
// Unexpected: 100 → 200 → 210 → 220
// Expected: 100 → 110 → 210 → 220
Why: During logTransaction, second deposit runs, modifying balance before first completes.
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.
Course Deep Dive: This topic is covered in detail in Lesson 7.3: Understanding Task suspension points
Tasks run on cooperative thread pool (background threads):
Task {
print(Thread.current) // Background thread
}
Use @MainActor for main thread:
@MainActor
func updateUI() {
Task {
print(Thread.current) // Main thread
}
}
@MainActor
func 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)")
}
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)
}
}
@MainActor
func caller() async {
let obj = NotSendable()
await obj.performAsync()
// Old: Background thread
// New: Main thread (inherits @MainActor)
}
In Xcode 16+:
// Build setting or swift-settings
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
Force function to switch away from caller's isolation:
@concurrent
func performAsync() async {
print(Thread.current) // Always background
}
Prevent sending non-Sendable values across isolation:
nonisolated(nonsending) func storeTouch(...) async {
// Runs on caller's isolation, no value sending
}
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)
Use when: Method doesn't need to switch isolation, avoiding Sendable requirements.
Build setting (Xcode 16+):
MainActor or NoneSwift Package:
.target(
name: "MyTarget",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
Most app code runs on main thread. Setting @MainActor as default:
// With @MainActor as default:
func f() {} // Inferred: @MainActor
class C {
init() {} // Inferred: @MainActor
static var value = 10 // Inferred: @MainActor
}
@MyActor
struct S {
func f() {} // Inferred: @MyActor (explicit override)
}
> **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)
Must opt in for each module/package. Not global across dependencies.
Opt-in only. Default remains nonisolated if not specified.
⚠️ 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)")
assert(Thread.isMainThread)
Wrong. Tasks share limited thread pool, reuse threads.
Wrong. await suspends task without blocking thread. Other tasks can use the thread.
Wrong. Tasks execute based on system scheduling. Use await to enforce order.
Wrong. Task can resume on different thread after suspension.
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.
@MainActorNonisolatedNonsendingByDefault@concurrent for explicit background work@concurrent to maintain old behavior where neededNeed 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 refactor
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 @concurrent if it must leave caller isolation)DispatchQueue(label:).sync { } → actor or Mutex for protecting stateactor (guarantees serial access)@MainActoractorasync API with explicit ownership@concurrentawait as a blocking call — it suspends the task, freeing the thread.Task to a conceptual thread.For migration strategies, real-world examples, and advanced threading patterns, see Swift Concurrency Course.