Thread Safety Guarantees in FluidAudio Managers: Swift Actor Implementation Explained
FluidAudio managers use Swift actors to provide compiler-enforced thread safety, automatically serializing access to mutable state and eliminating data races without requiring manual locks or queues.
The fluidinference/fluidaudio repository implements all public manager types—including AsrManager, VadManager, StreamingAsrManager, DiarizerManager, and PocketTtsManager—as Swift actors. This design delivers strong thread safety guarantees for concurrent audio processing, ensuring that mutable internal state remains consistent even when accessed from multiple concurrent tasks.
Core Thread Safety Mechanisms
Actor-Based State Isolation
Every public manager in FluidAudio is declared as a Swift actor, which isolates mutable stored properties to a private serial executor. In Sources/FluidAudio/VAD/VadManager.swift, the declaration public actor VadManager at lines 14-15 establishes this isolation boundary. This guarantees that all mutations of the manager's internal state happen sequentially, preventing data races by design.
The serial execution model ensures that every async method on the manager runs one-at-a-time, even when invoked from many concurrent tasks. Callers do not need to manage dispatch queues or locks manually; the actor automatically thread-hops to its private executor when receiving method calls.
Sendable Conformance Verification
FluidAudio constrains all public API types—including model structs like AsrModels, DiarizerModels, and error enums—to conform to Sendable. The test suite explicitly validates this conformance in Tests/FluidAudioTests/SendableTests.swift at lines 7-26, ensuring that manager instances can be safely passed across concurrency domains without introducing shared mutable state.
This combination of actor isolation and Sendable conformance allows FluidAudio managers to participate in structured concurrency patterns while maintaining compile-time safety guarantees.
Practical Implementation Patterns
Concurrent ASR and VAD Processing
Because managers are actors, you can safely invoke them from concurrent tasks without synchronization primitives. The following pattern demonstrates safe concurrent usage of AsrManager and VadManager:
import FluidAudio
let asr = AsrManager()
let vad = VadManager()
let audioURL = Bundle.main.url(forResource: "sample", withExtension: "wav")!
Task {
// Runs serially on AsrManager's private executor
let models = try await AsrModels.loadFromBundle()
try await asr.initialize(models: models)
let results = try await asr.transcribe(audioURL: audioURL)
print("ASR:", results.text)
}
Task {
// Runs serially on VadManager's private executor
let vadResults = try await vad.process(audioURL)
print("VAD chunks:", vadResults.count)
}
In Sources/FluidAudio/ASR/AsrManager.swift, the manager confines mutable state—such as asrModels and decoder states—to its serial executor. Similarly, VadManager processes audio chunks sequentially despite concurrent task invocation.
Streaming ASR with Multiple Sessions
For real-time applications, StreamingAsrManager and StreamingAsrSession are also declared as actors. In Sources/FluidAudio/ASR/Streaming/StreamingAsrManager.swift (lines 7-8) and StreamingAsrSession.swift (lines 6-7), the actor pattern ensures that each streaming session maintains consistent internal state even when processing interleaved audio chunks from concurrent sources:
let streaming = StreamingAsrManager()
Task {
let session = await streaming.createSession()
for chunk in audioChunks {
try await session.process(chunk)
}
let transcript = await session.finish()
print(transcript)
}
Each session operates as an independent actor, providing isolation between concurrent streams while the manager itself serializes high-level operations.
Safely Handling Non-Thread-Safe Utilities
While managers provide strong safety guarantees, certain low-level utilities remain non-thread-safe by design. Classes like NeMoMelSpectrogram and WhisperMelSpectrogram contain cache-free buffers that are documented as not thread-safe in Sources/FluidAudio/ASR/Streaming/NeMoMelSpectrogram.swift at lines 19-22.
To use these utilities safely, create fresh instances per task rather than sharing them across concurrency domains:
Task {
// Create a new instance for each concurrent operation
let mel = NeMoMelSpectrogram()
let features = try mel.compute(inputBuffer)
// Pass features to an actor for processing
let result = try await asr.transcribe(features: features)
print(result.text)
}
This pattern isolates mutable buffer state within a single task, then transfers immutable Sendable data (the features) to the actor-based manager for inference.
Summary
- FluidAudio managers are Swift actors: Declarations like
public actor VadManagerinSources/FluidAudio/VAD/VadManager.swiftenforce automatic serialization of mutable state. - Compiler-enforced safety: The
Sendableconformance verified inTests/FluidAudioTests/SendableTests.swiftensures safe cross-domain passing of managers and their associated types. - Zero manual synchronization required: Actor isolation eliminates the need for locks, queues, or atomic operations when calling manager methods from concurrent tasks.
- Explicit non-thread-safe boundaries: Utilities such as
NeMoMelSpectrogramare documented as requiring per-task instantiation, with all other manager APIs providing full thread safety.
Frequently Asked Questions
Are FluidAudio managers thread-safe by default?
Yes. All public manager types—including AsrManager, VadManager, and StreamingAsrManager—are implemented as Swift actors. According to the source code in Sources/FluidAudio/VAD/VadManager.swift and related files, this actor declaration guarantees that mutable internal state is accessed serially, preventing data races without requiring manual synchronization from callers.
Can I call a FluidAudio manager from multiple concurrent tasks simultaneously?
Yes. Because the managers are actors, they automatically serialize incoming method calls. Even when you invoke asr.transcribe() or vad.process() from multiple concurrent Task blocks, the actor's private executor ensures these calls execute one-at-a-time, maintaining state consistency while allowing natural concurrency patterns in your calling code.
What parts of FluidAudio are not thread-safe?
Only specific low-level utilities such as NeMoMelSpectrogram and WhisperMelSpectrogram are documented as non-thread-safe, as noted in Sources/FluidAudio/ASR/Streaming/NeMoMelSpectrogram.swift. These classes use cache-free buffers that must not be shared across threads. The solution is to instantiate these objects fresh for each concurrent task, then pass their output to the thread-safe actor-based managers for further processing.
Do I need to use @MainActor or DispatchQueue with FluidAudio managers?
No. FluidAudio managers manage their own executors automatically. Since they are declared as actors (e.g., public actor StreamingAsrManager in Sources/FluidAudio/ASR/Streaming/StreamingAsrManager.swift), method calls automatically dispatch to the actor's private serial executor. You can invoke these managers from any concurrency context—main thread, background tasks, or structured concurrency groups—without manually specifying dispatch queues or @MainActor annotations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →