Worker Patterns
Design patterns for managing background processing tasks that need to run off the main thread while maintaining thread safety and coordinated access to shared resources. Essential for real-time applications like audio processing where blocking the main thread would cause UI freezes.
Core Concepts
Serial Dispatch Queues: Create dedicated queues for specific processing tasks to ensure operations execute in order without blocking the main thread.
private let wakeWordQueue = DispatchQueue(label: "wake-word-inference", qos: .userInitiated)
Worker Class Separation: Extract heavy processing logic into dedicated worker classes that own their execution context and communicate back to the main thread via callbacks.
class WakeWordInferenceWorker {
private let queue = DispatchQueue(label: "inference-worker")
private let pipeline: OpenWakeWordPipeline
func processAudio(_ samples: [Float]) {
queue.async { [weak self] in
// Heavy processing off main thread
let result = self?.pipeline.process(samples)
DispatchQueue.main.async {
// Callback to main thread
self?.delegate?.wakeWordDetected(result)
}
}
}
}
Swift Concurrency Integration
Actor Isolation Management: With Swift's strict concurrency model, worker patterns help manage @MainActor isolation by clearly separating concerns.
@MainActor
class AudioController {
nonisolated private let worker = WakeWordInferenceWorker()
nonisolated func audioTap(buffer: AVAudioPCMBuffer) {
// Can safely call worker from audio thread
worker.processAudio(extractSamples(buffer))
}
}
Quality of Service: Use appropriate QoS classes for worker queues based on processing urgency:
.userInitiatedfor real-time audio processing.utilityfor background analysis.backgroundfor non-urgent batch processing
Common Patterns
Audio Processing Worker
Handles continuous audio stream processing without blocking the main thread:
class AudioProcessingWorker {
private let processingQueue = DispatchQueue(label: "audio-processing", qos: .userInitiated)
private var audioBuffer: CircularBuffer<Float> = CircularBuffer(capacity: 16000)
func enqueueAudio(_ samples: [Float]) {
processingQueue.async { [weak self] in
self?.audioBuffer.append(samples)
self?.processBufferIfReady()
}
}
}
Rolling Buffer Management
Maintain sliding windows of data for streaming inference:
class RollingBufferWorker<T> {
private var buffer: [T] = []
private let maxSize: Int
private let queue = DispatchQueue(label: "buffer-worker")
func append(_ items: [T], completion: @escaping ([T]) -> Void) {
queue.async { [weak self] in
guard let self = self else { return }
self.buffer.append(contentsOf: items)
if self.buffer.count > self.maxSize {
self.buffer.removeFirst(self.buffer.count - self.maxSize)
}
DispatchQueue.main.async {
completion(Array(self.buffer))
}
}
}
}
Best Practices
Weak Self: Always use [weak self] in async closures to prevent retain cycles.
Main Thread Callbacks: Process results on the main thread for UI updates, but do heavy lifting in worker queues.
Queue Naming: Use descriptive queue labels for debugging and profiling.
Error Handling: Propagate errors back to main thread through completion handlers or delegate methods.
Resource Cleanup: Ensure worker queues can be properly disposed of when parent objects are deallocated.