Swift Concurrency
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
- MainActor: UI components, user interaction handlers
- Background Queues: Heavy computation, I/O operations
- Serial Queues: Stateful processing like audio pipelines
- 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
awaitplacement - Batch operations to reduce context switching overhead
See also
- nonisolated-methods
- worker-patterns
- onnx-runtime
- AVAudioEngine
- xiexie-senior-safety-app