~/wiki

Main Actor Isolation

Mis à jour le 2025-12-25Confiance : high
main-actor-isolationswift-concurrencymainactorthread-safetyui-programmingactor-isolationdefault-isolationswift-6

Swift concurrency pattern where classes and methods are automatically isolated to the main thread through the @MainActor annotation or project-level default settings. Critical for UI programming but creates challenges for background processing.

Core Concept

The MainActor is a global actor representing the main dispatch queue. When code is main-actor-isolated, it:

  • Runs exclusively on the main thread
  • Has guaranteed serial execution with other main-actor code
  • Can safely access UI elements without data races
  • Requires await when called from other contexts

Configuration Patterns

Explicit Annotation

@MainActor
class UIController {
    func updateInterface() {
        // Guaranteed to run on main thread
    }
}

Project-Level Default

Many iOS/macOS projects set SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, making all classes main-actor-isolated unless explicitly opted out:

// Implicitly @MainActor due to project setting
class ViewManager {
    func handleUserAction() { /* Main thread */ }
}

// Explicitly opt out of main actor
nonisolated class DataProcessor {
    func processInBackground() { /* Any thread */ }
}

Challenges for Background Processing

Audio Processing Conflicts

Real-time audio processing requires background execution but main-actor isolation forces everything onto the main thread:

@MainActor  // Problem: audio callbacks need background execution
class WakeWordDetector {
    func setupAudio() {
        audioEngine.inputNode.installTap { buffer, _ in
            // ERROR: This callback runs on audio thread
            // but class is main-actor-isolated
            self.processAudio(buffer)
        }
    }
}

Worker Queue Integration

Background queues cannot directly access main-actor-isolated state:

@MainActor
class AudioManager {
    private var pipeline: AudioPipeline?
    
    func startProcessing() {
        DispatchQueue.global().async {
            // ERROR: Cannot access main-actor property
            self.pipeline?.process()
        }
    }
}

Solutions and Patterns

Nonisolated Escape Hatches

nonisolated-methods allow specific methods to execute outside main-actor isolation:

@MainActor
class CompanionManager {
    nonisolated func handleWakeWord() {
        // Can be called from any thread
        DispatchQueue.main.async {
            // Hop back to main actor for UI updates
        }
    }
}

Separate Worker Classes

Extract non-UI logic into separate classes that aren't main-actor-isolated:

@MainActor
class UIController {
    private let worker = AudioWorker() // Not main-actor-isolated
    
    func startAudio() {
        worker.startProcessing { [weak self] result in
            // Callback from background, hop to main
            DispatchQueue.main.async {
                self?.updateUI(result)
            }
        }
    }
}

class AudioWorker {
    func startProcessing(completion: @escaping (Result) -> Void) {
        // Runs on background queue
    }
}

Unsafe Access Patterns

For truly thread-safe resources, nonisolated(unsafe) bypasses isolation:

@MainActor
class AudioProcessor {
    private nonisolated(unsafe) let pipeline: ThreadSafePipeline
    
    func processFromBackground() {
        // Can be called from any thread
        // Only safe if pipeline is truly thread-safe
        pipeline.process()
    }
}

Performance Implications

Context Switching Overhead

Frequent actor hopping creates performance penalties:

// Expensive: multiple main actor hops
for sample in audioSamples {
    await mainActorProcessor.process(sample)
}

// Better: batch processing
await mainActorProcessor.processBatch(audioSamples)

Queue Congestion

Main actor isolation can create bottlenecks when background work must hop to main thread for simple operations.

Best Practices

Minimal Surface Area

Keep main-actor-isolated classes focused on UI concerns:

@MainActor
class ConversationViewController {
    // UI-only responsibilities
    func updateTranscript(_ text: String) { }
    func showError(_ error: Error) { }
}

// Background processing in separate class
class AudioProcessor {
    func processAudio() -> String { }
}

Strategic Nonisolated

Use nonisolated for thread-safe operations and callbacks:

@MainActor
class AudioManager {
    nonisolated func handleAudioCallback() {
        // Thread-safe coordination
    }
    
    func updateUI() {
        // Main-actor UI work
    }
}

Clear Boundaries

Establish clear ownership boundaries between main-actor and background components to avoid complex isolation juggling.

See also