6.9 KiB
Threading Model
This document describes CubicSDR's threading architecture, synchronization mechanisms, and thread lifecycle management.
Thread Infrastructure
IOThread Base Class
File: src/IOThread.h
All worker threads inherit from IOThread, which provides:
- Lifecycle management:
stopping(atomic bool) for async termination;terminated(atomic bool) for completion - Named queue bindings:
setInputQueue(name, queue)/setOutputQueue(name, queue)with string-keyed maps - Thread entry:
threadMain()wrapsrun()in try/catch, setsterminated=trueon exit - Spin-wait sleep:
isTerminated(timeout)busy-waits with 5ms sleep (SPIN_WAIT_SLEEP_MS) between checks
Thread Creation Pattern
CubicSDR uses std::thread exclusively (no wxThread):
threadObject = new ThreadClass(...);
t_stdThread = new std::thread(&ThreadClass::threadMain, threadObject);
On macOS, DemodulatorPreThread and DemodulatorThread use pthread_create with ~2MB stack sizes (2,048,000 bytes).
Thread Inventory
| Thread | Class | File | Purpose |
|---|---|---|---|
| Main (wxWidgets UI) | wxApp/wxFrame | AppFrame.cpp |
Event loop, OpenGL rendering, UI interaction |
| SDR Thread | SDRThread |
src/sdr/SoapySDRThread.h |
Reads IQ samples from SoapySDR hardware |
| SDR Post-Processing | SDRPostThread |
src/sdr/SDRPostThread.h |
DC blocking, channelization, IQ distribution |
| SDR Enumerator | SDREnumerator |
src/sdr/SDREnumerator.h |
Background device discovery |
| Spectrum Visual | SpectrumVisualDataThread |
src/process/SpectrumVisualDataThread.h |
FFT processing for main spectrum display |
| Demod Visual | SpectrumVisualDataThread |
src/process/SpectrumVisualDataThread.h |
FFT processing for demod spectrum display |
| Waterfall FFT | FFTVisualDataThread |
src/process/FFTVisualDataThread.h |
Rate-limited waterfall FFT data |
| Demodulator Pre-Thread (N) | DemodulatorPreThread |
src/demod/DemodulatorPreThread.h |
Frequency shifting, resampling, modem switching |
| Demodulator Worker (N) | DemodulatorWorkerThread |
src/demod/DemodulatorWorkerThread.h |
Offloaded modem/filter creation |
| Demodulator Thread (N) | DemodulatorThread |
src/demod/DemodulatorThread.h |
Core demodulation, squelch, signal levels |
| Audio Thread (N) | AudioThread |
src/audio/AudioThread.h |
Per-demod audio processing and device binding |
| Audio Controller | AudioThread |
src/audio/AudioThread.h |
Per-device RtAudio stream owner and mixer |
| Audio Sink (N) | AudioSinkThread |
src/audio/AudioSinkThread.h |
WAV file recording |
| Rig Control (optional) | RigThread |
src/rig/RigThread.h |
Hamlib CAT control |
Communication Patterns
Pattern 1: Queue-Based Data Flow (Primary)
All data-carrying threads communicate via ThreadBlockingQueue<T>:
- Blocking push/pop for critical data (demod pipeline, worker commands)
- Non-blocking try_push/try_pop for visualization (data loss acceptable)
- All data passed as
std::shared_ptr<T>
Pattern 2: Atomic Flags for Control
std::atomic_bool flags signal parameter changes between UI and worker threads:
freq_changed,rate_changed,bandwidthChangeddemodTypeChanged,modemSettingsChanged- The polling thread checks flags each iteration and applies changes
Pattern 3: Controller/Bound Audio Mixing
- One AudioThread per physical output device owns the RtAudio stream (controller)
- Other AudioThreads bind to the controller via
bindThread() - The
audioCallback(real-time context) iterates bound threads, pops audio viatry_pop(), and mixes into the output buffer
Pattern 4: Worker Thread for Expensive Operations
DemodulatorWorkerThreadhandles modem creation and filter building- PreThread sends commands via bounded queue (max 2 items)
- Worker responds with results via separate queue (max 100 items)
- Prevents expensive liquid-dsp initialization from blocking the data path
Synchronization Mechanisms
| Mechanism | Location | Purpose |
|---|---|---|
ThreadBlockingQueue<T> |
Throughout | Primary inter-thread data transfer |
SpinMutex |
ThreadBlockingQueue, ReBuffer |
Lightweight lock for high-frequency queue operations |
std::atomic_bool |
SDRThread, DemodulatorPreThread, DemodulatorThread | Lock-free parameter change signaling |
std::recursive_mutex |
AudioThread, DemodulatorInstance, DemodulatorMgr | Protecting shared mutable state |
std::mutex |
IOThread queue bindings, SDRThread settings | Protecting infrequent mutations |
SpinMutex
File: src/util/SpinMutex.h
Non-recursive spinlock using std::atomic_flag with acquire/release memory ordering. Used as the internal lock for ThreadBlockingQueue and ReBuffer due to its low overhead for short critical sections.
Thread Lifecycle
Startup Sequence
In CubicSDR::OnInit() (src/CubicSDR.cpp):
- Queue plumbing — all global queues created and wired
SpectrumVisualDataThreadstartedDemodVisualDataThreadstarted (if enabled)SDRPostThreadstartedAppFramecreated (wxWidgets main window)SDREnumeratorstarted- Device selection triggers
SDRThreadstart (inCubicSDR::setDevice())
Per-Demodulator Startup
When DemodulatorInstance::run() is called:
AudioThreadstartedDemodulatorPreThreadstartedDemodulatorThreadstarted
Shutdown Sequence
In CubicSDR::OnExit():
SDRThread::terminate()— stops producing IQ data (waited up to 3s)SDRPostThread::terminate()— stops channelizing (waited up to 3s)DemodulatorMgr::terminateAll()— terminates all demodulator instances- Visual processor threads terminated
- All threads joined
- All queues flushed to unblock pending operations
Per-Demodulator Shutdown
DemodulatorInstance::terminate():
AudioThread::terminate()— stops consuming audioDemodulatorThread::terminate()— stops demodulatingDemodulatorPreThread::terminate()— stops resampling (also terminates worker thread)- All queues flushed to unblock pending pushes
Thread Priorities (macOS)
On macOS, threads are assigned scheduling priorities:
| Thread | Policy | Priority |
|---|---|---|
| SDR Post-Processing | SCHED_FIFO |
sched_get_priority_max |
| Demodulator Pre-Thread | SCHED_FIFO |
max - 1 |
| Demodulator Thread | SCHED_FIFO |
max - 1 |
| Audio Thread (controller) | SCHED_RR |
max - 1 |
Note: SDR Thread has SCHED_FIFO priority code but it is currently commented out.
Windows and Linux use default thread priorities.
wxWidgets Integration
The UI thread is entirely pull-based:
AppFrame::OnIdle()is called continuously by the wx event loop- Each canvas calls
processInputQueue()which usestry_pop()(non-blocking) - Worker threads never push data to the UI
- Shared state uses
std::atomicvariables (frequency, signal levels, mute state) - UI-initiated changes go through atomic variables and flags, not wx events