~/wiki

Swift Concurrency

Mis à jour le 2025-12-25Confiance : high
swift-concurrencyactor-isolationmainactornonisolatedasync-awaitthread-safetyios-developmentaudio-processingonnx-runtimereal-time-inferencedispatch-queuesworker-patternsswift-6concurrency-modelserial-queuesdefault-isolationunsafe-patternsmain-actor-isolationconcurrency-warnings

Modern concurrency system in Swift providing actor-based isolation, structured concurrency, and compile-time thread safety guarantees. Essential for building responsive iOS/macOS applications that handle multiple concurrent operations safely.

Core Concepts

Actor Isolation

  • Actors: Reference types that protect their mutable state by serializing access
  • MainActor: Global actor representing the main thread, required for UI updates
  • Isolated Methods: Can only be called from within the same actor context
  • Nonisolated Methods: Can be called from any context without actor hopping

Async/Await

  • Async Functions: Methods that can suspend execution and resume later
  • Await: Keyword for calling async functions and potentially yielding control
  • Structured Concurrency: Task hierarchies with automatic cancellation propagation

Real-World Challenges

MainActor Default Isolation

In projects with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, all classes become main-actor-isolated by default unless explicitly marked otherwise. This creates challenges when:

// This class is implicitly @MainActor
class AudioProcessor {
    func processAudio() { /* Must run on main thread */ }
}

// Worker queue access requires careful handling
let processor = AudioProcessor()
DispatchQueue.global().async {
    // ERROR: Main actor-isolated instance cannot be accessed
    processor.processAudio()
}

Audio Processing Patterns

Real-time audio processing requires background execution but must coordinate with main-actor UI updates:

@MainActor
class WakeWordDetector {
    private let inferenceWorker = WakeWordInferenceWorker()
    
    // Audio tap runs on audio thread, needs careful bridging
    private func setupAudioTap() {
        audioEngine.inputNode.installTap(onBus: 0) { [weak self] buffer, _ in
            // This callback runs on audio thread
            self?.inferenceWorker.processAudio(buffer)
        }
    }
}

// Separate worker for background processing
class WakeWordInferenceWorker {
    private let processingQueue = DispatchQueue(label: "wake-word")
    
    func processAudio(_ buffer: AVAudioPCMBuffer) {
        processingQueue.async { [weak self] in
            self?.runInference(buffer)
        }
    }
}

Nonisolated Patterns

nonisolated-methods allow safe access across actor boundaries:

@MainActor
class CompanionManager {
    private nonisolated(unsafe) var pipeline: OpenWakeWordPipeline?
    
    nonisolated func handleWakeWordDetected() {
        // Can be called from any thread/queue
        DispatchQueue.main.async { [weak self] in
            self?.startConversation()
        }
    }
}

Worker Patterns in Practice

Serial Queue Architecture

worker-patterns for real-time processing while maintaining thread safety:

class WakeWordInferenceWorker {
    private let processingQueue = DispatchQueue(
        label: "wake-word-inference",
        qos: .userInitiated
    )
    private var pipeline: OpenWakeWordPipeline?
    
    func processAudioSamples(_ samples: [Float]) {
        processingQueue.async { [weak self] in
            guard let pipeline = self?.pipeline else { return }
            
            if pipeline.detectWakeWord(samples) {
                // Fire callback on main thread
                DispatchQueue.main.async {
                    self?.delegate?.wakeWordDetected()
                }
            }
        }
    }
}

Memory Management

Careful weak references prevent retain cycles between actors:

// Audio callback uses weak self to prevent cycles
audioEngine.inputNode.installTap { [weak self] buffer, _ in
    self?.handleAudioBuffer(buffer)
}

// Worker callbacks also use weak references
processingQueue.async { [weak self] in
    guard let self = self else { return }
    // Safe to use self here
}

Integration with ONNX Runtime

Thread Safety Considerations

onnx-runtime models are not thread-safe and require careful coordination:

class OpenWakeWordPipeline {
    private var melModel: ORTSession?
    private var embeddingModel: ORTSession?
    private var classifierModel: ORTSession?
    
    // All inference must happen on same serial queue
    func detectWakeWord(_ samples: [Float]) -> Bool {
        // This method assumes it's called from a serial queue
        precondition(DispatchQueue.getSpecific(key: processingKey) != nil)
        
        // Safe to use models sequentially
        let melOutput = try melModel?.run(...)
        let embeddingOutput = try embeddingModel?.run(...)
        let score = try classifierModel?.run(...)
        
        return score > threshold
    }
}

Best Practices

Isolation Strategy

  1. MainActor: UI components, user interaction handlers
  2. Background Queues: Heavy computation, I/O operations
  3. Serial Queues: Stateful processing like audio pipelines
  4. Nonisolated: Thread-safe data access and callbacks

Error Handling

// Async methods should handle isolation errors
@MainActor
func startRecording() async {
    do {
        try await audioEngine.start()
        setupWakeWordDetection()
    } catch {
        // Handle on main actor for UI updates
        showError(error)
    }
}

Performance Optimization

  • Use nonisolated(unsafe) sparingly and only for truly thread-safe access
  • Minimize actor hopping with strategic await placement
  • Batch operations to reduce context switching overhead

See also