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/actors.md
1# Actors23Use this when:45- You need to protect class-based mutable state from concurrent access.6- You are choosing between `actor`, `@MainActor`, `nonisolated`, or `Mutex`.7- You are resolving protocol conformance issues on actor-isolated types.89Skip this file if:1011- You mainly need to make a value safe to transfer across boundaries. Use `sendable.md`.12- You are debugging execution threads or suspension behavior. Use `threading.md`.1314Jump to:1516- Actor Isolation17- Global Actors / @MainActor18- Isolated vs Nonisolated19- Actor Reentrancy20- Isolated Deinit / Isolated Conformances (Swift 6.2+)21- `#isolation` Macro22- Mutex: Alternative to Actors23- Decision Tree2425## What is an Actor?2627Actors protect mutable state by ensuring only one task accesses it at a time. They're reference types with automatic synchronization.2829```swift30actor Counter {31var value = 03233func increment() {34value += 135}36}37```3839**Key guarantee**: Only one task can access mutable state at a time (serialized access).4041> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.1: Understanding actors in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)4243## Actor Isolation4445### Enforced by compiler4647```swift48actor BankAccount {49var balance: Int = 05051func deposit(_ amount: Int) {52balance += amount53}54}5556let account = BankAccount()57account.balance += 1 // ❌ Error: can't mutate from outside58await account.deposit(1) // ✅ Must use actor's methods59```6061### Reading properties6263```swift64let account = BankAccount()65await account.deposit(100)66print(await account.balance) // Must await reads too67```6869Always use `await` when accessing actor properties/methods—you don't know if another task is inside.7071## Actors vs Classes7273### Similarities7475- Reference types (copies share same instance)76- Can have properties, methods, initializers77- Can conform to protocols7879### Differences8081- **No inheritance** (except `NSObject` for Objective-C interop)82- **Automatic isolation** (no manual locks needed)83- **Implicit Sendable** conformance8485```swift86// ❌ Can't inherit from actors87actor Base {}88actor Child: Base {} // Error8990// ✅ NSObject exception91actor Example: NSObject {} // OK for Objective-C92```9394## Global Actors9596Shared isolation domain across types, functions, and properties.9798### @MainActor99100Ensures execution on main thread:101102```swift103@MainActor104final class ViewModel {105var items: [Item] = []106}107108@MainActor109func updateUI() {110// Always runs on main thread111}112113@MainActor114var title: String = ""115```116117### Custom global actors118119```swift120@globalActor121actor ImageProcessing {122static let shared = ImageProcessing()123private init() {} // Prevent duplicate instances124}125126@ImageProcessing127final class ImageCache {128var images: [URL: Data] = [:]129}130131@ImageProcessing132func applyFilter(_ image: UIImage) -> UIImage {133// All image processing serialized134}135```136137**Use private init** to prevent creating multiple executors.138139> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.2: An introduction to Global Actors](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)140141## @MainActor Best Practices142143### When to use144145UI-related code that must run on main thread:146147```swift148@MainActor149final class ContentViewModel: ObservableObject {150@Published var items: [Item] = []151}152```153154### Replacing DispatchQueue.main155156```swift157// Old way158DispatchQueue.main.async {159// Update UI160}161162// Modern way163await MainActor.run {164// Update UI165}166167// Better: Use attribute168@MainActor169func updateUI() {170// Automatically on main thread171}172```173174### MainActor.assumeIsolated175176**Use sparingly** - assumes you're on main thread, crashes if not:177178```swift179func methodB() {180assert(Thread.isMainThread) // Validate assumption181182MainActor.assumeIsolated {183someMainActorMethod()184}185}186```187188**Prefer**: Explicit `@MainActor` or `await MainActor.run` over `assumeIsolated`.189190> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.3: When and how to use @MainActor](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)191192## Isolated vs Nonisolated193194### Default: Isolated195196Actor methods are isolated by default:197198```swift199actor BankAccount {200var balance: Double201202// Implicitly isolated203func deposit(_ amount: Double) {204balance += amount205}206}207```208209### Isolated parameters210211Reduce suspension points by inheriting caller's isolation:212213```swift214struct Charger {215static func charge(216amount: Double,217from account: isolated BankAccount218) async throws -> Double {219// No await needed - we're isolated to account220try account.withdraw(amount: amount)221return account.balance222}223}224```225226### Isolated closures227228```swift229actor Database {230func transaction<T>(231_ operation: @Sendable (_ db: isolated Database) throws -> T232) throws -> T {233beginTransaction()234let result = try operation(self)235commitTransaction()236return result237}238}239240// Usage: Multiple operations, one await241try await database.transaction { db in242db.insert(item1)243db.insert(item2)244db.insert(item3)245}246```247248### Generic isolated extension249250```swift251extension Actor {252func performInIsolation<T: Sendable>(253_ block: @Sendable (_ actor: isolated Self) throws -> T254) async rethrows -> T {255try block(self)256}257}258259// Usage260try await bankAccount.performInIsolation { account in261try account.withdraw(amount: 20)262print("Balance: \(account.balance)")263}264```265266### Nonisolated267268Opt out of isolation for immutable data:269270```swift271actor BankAccount {272let accountHolder: String273274nonisolated var details: String {275"Account: \(accountHolder)"276}277}278279// No await needed280print(account.details)281```282283### Protocol conformance284285```swift286extension BankAccount: CustomStringConvertible {287nonisolated var description: String {288"Account: \(accountHolder)"289}290}291```292293> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.4: Isolated vs. non-isolated access in actors](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)294295## Isolated Deinit (Swift 6.2+)296297Clean up actor state on deallocation:298299```swift300actor FileDownloader {301var downloadTask: Task<Void, Error>?302303isolated deinit {304downloadTask?.cancel() // Can call isolated methods305}306}307```308309**Requires**: iOS 18.4+, macOS 15.4+310311> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.5: Using Isolated synchronous deinit](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)312313## Global Actor Isolated Conformance (Swift 6.2+)314315Protocol conformance respecting actor isolation:316317```swift318@MainActor319final class PersonViewModel {320let id: UUID321var name: String322}323324extension PersonViewModel: @MainActor Equatable {325static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {326lhs.id == rhs.id && lhs.name == rhs.name327}328}329```330331**Enable**: `InferIsolatedConformances` upcoming feature.332333> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.6: Adding isolated conformance to protocols](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)334335## Actor Reentrancy336337**Critical**: State can change between suspension points.338339```swift340actor BankAccount {341var balance: Double342343func deposit(amount: Double) async {344balance += amount345346// ⚠️ Actor unlocked during await347await logActivity("Deposited \(amount)")348349// ⚠️ Balance may have changed!350print("Balance: \(balance)")351}352}353```354355### Problem356357```swift358async let _ = account.deposit(50)359async let _ = account.deposit(50)360async let _ = account.deposit(50)361362// May print same balance three times:363// Balance: 150364// Balance: 150365// Balance: 150366```367368### Solution369370Complete actor work before suspending:371372```swift373func deposit(amount: Double) async {374balance += amount375print("Balance: \(balance)") // Before suspension376377await logActivity("Deposited \(amount)")378}379```380381**Rule**: Don't assume state is unchanged after `await`.382383> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.7: Understanding actor reentrancy](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)384385## #isolation Macro386387Inherit caller's isolation for generic code:388389```swift390extension Collection where Element: Sendable {391func sequentialMap<Result: Sendable>(392isolation: isolated (any Actor)? = #isolation,393transform: (Element) async -> Result394) async -> [Result] {395var results: [Result] = []396for element in self {397results.append(await transform(element))398}399return results400}401}402403// Usage from @MainActor context404Task { @MainActor in405let names = ["Alice", "Bob"]406let results = await names.sequentialMap { name in407await process(name) // Inherits @MainActor408}409}410```411412**Benefits**: Avoids unnecessary suspensions, allows non-Sendable data.413414### Task Closures and Isolation Inheritance415416When spawning unstructured `Task` closures that need to work with `non-Sendable` types, you must capture the isolation parameter to inherit the caller's isolation context.417418**Problem**: `Task` closures are `@Sendable`, which prevents capturing `non-Sendable` types:419420```swift421func process(delegate: NonSendableDelegate) {422Task {423delegate.doWork() // ❌ Error: capturing non-Sendable type424}425}426```427428**Solution**: Use `#isolation` parameter and capture it inside the `Task`:429430```swift431func process(432delegate: NonSendableDelegate,433isolation: isolated (any Actor)? = #isolation434) {435Task {436_ = isolation // Forces capture, Task inherits caller's isolation437delegate.doWork() // ✅ Safe - running on caller's actor438}439}440```441442**Why `_ = isolation` is required**: Per [SE-0420](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0420-inheritance-of-actor-isolation.md), `Task` closures only inherit isolation when "a non-optional binding of an isolated parameter is captured by the closure." The `_ = isolation` statement forces this capture. The capture list syntax `[isolation]` should work but currently does not.443444**When to use this pattern**:445- Spawning `Task`s that work with `non-Sendable` delegate objects446- Fire-and-forget async work that needs access to caller's state447- Bridging callback-based APIs to async streams while keeping delegates alive448449**Note**: This pattern keeps the `non-Sendable` value alive and accessible within the `Task`. The `Task` runs on the caller's isolation domain, so no cross-isolation "sending" occurs.450451> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.8: Inheritance of actor isolation using the #isolation macro](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)452453## Custom Actor Executors454455**Advanced**: Control how actor schedules work.456457### Serial executor458459```swift460final class DispatchQueueExecutor: SerialExecutor {461private let queue: DispatchQueue462463init(queue: DispatchQueue) {464self.queue = queue465}466467func enqueue(_ job: consuming ExecutorJob) {468let unownedJob = UnownedJob(job)469let executor = asUnownedSerialExecutor()470471queue.async {472unownedJob.runSynchronously(on: executor)473}474}475}476477actor LoggingActor {478private let executor: DispatchQueueExecutor479480nonisolated var unownedExecutor: UnownedSerialExecutor {481executor.asUnownedSerialExecutor()482}483484init(queue: DispatchQueue) {485executor = DispatchQueueExecutor(queue: queue)486}487}488```489490### When to use491492- Integration with legacy DispatchQueue-based code493- Specific thread requirements (e.g., C++ interop)494- Custom scheduling logic495496**Default executor is usually sufficient.**497498> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.9: Using a custom actor executor](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)499500## Mutex: Alternative to Actors501502Synchronous locking without async/await overhead (iOS 18+, macOS 15+).503504### Basic usage505506```swift507import Synchronization508509final class Counter {510private let count = Mutex<Int>(0)511512var currentCount: Int {513count.withLock { $0 }514}515516func increment() {517count.withLock { $0 += 1 }518}519}520```521522### Sendable access to non-Sendable types523524```swift525final class TouchesCapturer: Sendable {526let path = Mutex<NSBezierPath>(NSBezierPath())527528func storeTouch(_ point: NSPoint) {529path.withLock { path in530path.move(to: point)531}532}533}534```535536### Error handling537538```swift539func decrement() throws {540try count.withLock { count in541guard count > 0 else {542throw Error.reachedZero543}544count -= 1545}546}547```548549### Mutex vs Actor550551| Feature | Mutex | Actor |552|---------|-------|-------|553| Synchronous | ✅ | ❌ (requires await) |554| Async support | ❌ | ✅ |555| Thread blocking | ✅ | ❌ (cooperative) |556| Fine-grained locking | ✅ | ❌ (whole actor) |557| Legacy code integration | ✅ | ❌ |558559**Use Mutex when**:560- Need synchronous access561- Working with legacy non-async APIs562- Fine-grained locking required563- Low contention, short critical sections564565**Use Actor when**:566- Can adopt async/await567- Need logical isolation568- Working in async context569570> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.10: Using a Mutex as an alternative to actors](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)571572## Common Patterns573574### View model with @MainActor575576```swift577@MainActor578final class ContentViewModel: ObservableObject {579@Published var items: [Item] = []580581func loadItems() async {582items = try await api.fetchItems()583}584}585```586587### Background processing with custom actor588589```swift590@ImageProcessing591final class ImageProcessor {592func process(_ images: [UIImage]) async -> [UIImage] {593images.map { applyFilters($0) }594}595}596```597598### Mixed isolation599600```swift601actor DataStore {602private var items: [Item] = []603604func add(_ item: Item) {605items.append(item)606}607608nonisolated func itemCount() -> Int {609// ❌ Can't access items610return 0611}612}613```614615### Transaction pattern616617```swift618actor Database {619func transaction<T>(620_ operation: @Sendable (_ db: isolated Database) throws -> T621) throws -> T {622beginTransaction()623defer { commitTransaction() }624return try operation(self)625}626}627```628629## Best Practices6306311. **Prefer actors over manual locks** for async code6322. **Use @MainActor for UI** - all view models, UI updates6333. **Minimize work in actors** - keep critical sections short6344. **Watch for reentrancy** - don't assume state unchanged after await6355. **Use nonisolated sparingly** - only for truly immutable data6366. **Avoid assumeIsolated** - prefer explicit isolation6377. **Custom executors are rare** - default is usually best6388. **Consider Mutex for sync code** - when async overhead not needed6399. **Complete actor work before suspending** - prevent reentrancy bugs64010. **Use isolated parameters** - reduce suspension points641642## Decision Tree643644```645Need thread-safe mutable state?646├─ Async context?647│ ├─ Single instance? → Actor648│ ├─ Global/shared? → Global Actor (@MainActor, custom)649│ └─ UI-related? → @MainActor650│651└─ Synchronous context?652├─ Can refactor to async? → Actor653├─ Legacy code integration? → Mutex654└─ Fine-grained locking? → Mutex655```656657## Further Learning658659For migration strategies, advanced patterns, and real-world examples, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).660661