一键导入
swift-concurrency
// Expert guidance on Swift Concurrency concepts. Use when working with async/await, Tasks, actors, MainActor, Sendable, isolation domains, or debugging concurrency compiler errors. Helps write safe concurrent Swift code.
// Expert guidance on Swift Concurrency concepts. Use when working with async/await, Tasks, actors, MainActor, Sendable, isolation domains, or debugging concurrency compiler errors. Helps write safe concurrent Swift code.
| name | swift-concurrency |
| description | Expert guidance on Swift Concurrency concepts. Use when working with async/await, Tasks, actors, MainActor, Sendable, isolation domains, or debugging concurrency compiler errors. Helps write safe concurrent Swift code. |
This skill provides expert guidance on Swift's concurrency system based on the mental models from Fucking Approachable Swift Concurrency.
Think of your app as an office building where isolation domains are private offices with locks:
You can't barge into someone's office. You knock (await) and wait.
An async function can pause. Use await to suspend until work finishes:
func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
For parallel work, use async let:
async let avatar = fetchImage("avatar.jpg")
async let banner = fetchImage("banner.jpg")
return Profile(avatar: try await avatar, banner: try await banner)
A Task is a unit of async work you can manage:
// SwiftUI - cancels when view disappears
.task { avatar = await downloadAvatar() }
// Manual task creation
Task { await saveProfile() }
// Parallel work with TaskGroup
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { avatar = try await downloadAvatar() }
group.addTask { bio = try await fetchBio() }
try await group.waitForAll()
}
Child tasks in a group: cancellation propagates, errors cancel siblings, waits for all to complete.
Swift asks "who can access this data?" not "which thread?". Three isolation domains:
For UI. Everything UI-related should be here:
@MainActor
class ViewModel {
var items: [Item] = [] // Protected by MainActor
}
Protect their own mutable state with exclusive access:
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) { balance += amount }
}
await account.deposit(100) // Must await from outside
Opts out of actor isolation. Cannot access actor's protected state:
actor BankAccount {
nonisolated func bankName() -> String { "Acme Bank" }
}
let name = account.bankName() // No await needed
Two build settings that simplify the mental model:
// Runs on MainActor (default)
func updateUI() async { }
// Runs on background (opt-in)
@concurrent func processLargeFile() async { }
Marks types safe to pass across isolation boundaries:
// Sendable - value type, each gets a copy
struct User: Sendable {
let id: Int
let name: String
}
// Non-Sendable - mutable class state
class Counter {
var count = 0
}
Automatically Sendable:
For thread-safe classes with internal synchronization:
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
}
With Approachable Concurrency, isolation flows from MainActor through your code:
When writing generic async functions that accept closures, you need to preserve the caller's isolation to avoid Sendable errors.
Option 1: nonisolated(nonsending) (simpler)
// Stays on caller's executor, no Sendable needed
nonisolated(nonsending)
func measure<T>(_ label: String, block: () async throws -> T) async rethrows -> T
Option 2: #isolation parameter (when you need actor access)
// Explicit isolation parameter, useful if you need to pass it around
func measure<T>(
isolation: isolated (any Actor)? = #isolation,
_ label: String,
block: () async throws -> T
) async rethrows -> T
Use nonisolated(nonsending) by default. Use #isolation when you need explicit access to the actor instance.
// Still blocks main thread!
@MainActor func slowFunction() async {
let result = expensiveCalculation() // Synchronous = blocking
}
// Fix: Use @concurrent for CPU-heavy work
Most things can live on MainActor. Only create actors when you have shared mutable state that can't be on MainActor.
Not everything needs to cross boundaries. Step back and ask if data actually moves between isolation domains.
// Unnecessary
await MainActor.run { self.data = data }
// Better - annotate the function
@MainActor func loadData() async { self.data = await fetchData() }
Never use DispatchSemaphore, DispatchGroup.wait() in async code. Risks deadlock.
// Bad - unstructured
Task { await fetchUsers() }
Task { await fetchPosts() }
// Good - structured concurrency
async let users = fetchUsers()
async let posts = fetchPosts()
await (users, posts)
| Keyword | Purpose |
|---|---|
async | Function can pause |
await | Pause here until done |
Task { } | Start async work, inherits context |
Task.detached { } | Start async work, no context |
@MainActor | Runs on main thread |
actor | Type with isolated mutable state |
nonisolated | Opts out of actor isolation |
nonisolated(nonsending) | Stay on caller's executor |
Sendable | Safe to pass between isolation domains |
@concurrent | Always run on background (Swift 6.2+) |
#isolation | Capture caller's isolation as parameter |
async let | Start parallel work |
TaskGroup | Dynamic parallel work |
Trace the isolation: Where did it come from? Where is code trying to run? What data crosses a boundary?
The answer is usually obvious once you ask the right question.