iPlug2 plugins run on multiple threads simultaneously. Understanding thread safety is critical for stable, glitch-free plugins.
Thread Architecture
iPlug2 plugins typically operate on three threads:
Audio Thread Realtime-critical
ProcessBlock()
MIDI processing
DSP calculations
Main/UI Thread Non-realtime
UI drawing
OnIdle()
User interactions
Background Threads Optional
File I/O
Preset scanning
Convolution IR loading
The audio thread must never block or allocate memory. Blocking causes audio dropouts (clicks/pops).
Realtime-Safe Rules
Code running on the audio thread must follow strict rules:
❌ Never Do This on Audio Thread
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// ❌ Memory allocation
std ::vector < float > buffer (nFrames);
// ❌ Locks/mutexes
std ::lock_guard < std ::mutex > lock (mMutex);
// ❌ File I/O
mFile . read (buffer, size);
// ❌ System calls
printf ( "Processing audio \n " );
// ❌ Memory deallocation
delete[] mOldBuffer;
}
✅ Always Do This Instead
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// ✅ Stack allocation (fixed size)
float buffer [ 512 ];
// ✅ Lock-free communication
mQueue . Push (data);
// ✅ Atomic operations
mGain . store (newGain, std ::memory_order_release);
// ✅ Pre-allocated memory
mBuffer . Get ()[ 0 ] = value; // WDL_TypedBuf allocated in OnReset()
}
Thread-Safe Communication
Atomics for Simple Values
Use std::atomic for single values:
class MyPlugin : public Plugin
{
public:
void OnParamChange ( int paramIdx ) override
{
// Main thread - store value
mAtomicGain . store ( GetParam (kParamGain)-> Value (),
std ::memory_order_release);
}
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// Audio thread - read value
double gain = mAtomicGain . load ( std ::memory_order_acquire);
for ( int s = 0 ; s < nFrames; s ++ )
{
outputs [ 0 ][s] = inputs [ 0 ][s] * gain;
outputs [ 1 ][s] = inputs [ 1 ][s] * gain;
}
}
private:
std ::atomic < double > mAtomicGain { 1.0 };
};
Only use atomics for primitive types (int, float, double, bool, pointers). Not for complex objects.
IPlugQueue for Complex Data
For structured data, use IPlugQueue - a lock-free SPSC queue:
template < typename T >
class IPlugQueue
{
public:
IPlugQueue ( int size );
bool Push ( const T & item ); // Producer (e.g., audio thread)
bool Pop ( T & item ); // Consumer (e.g., main thread)
size_t ElementsAvailable () const ;
};
Example: Sending MIDI events to UI
class MyPlugin : public Plugin
{
public:
MyPlugin ( const InstanceInfo & info )
: Plugin (info, MakeConfig (kNumParams, kNumPresets))
, mMidiQueue ( 128 ) // Queue capacity
{
}
void ProcessMidiMsg ( const IMidiMsg & msg ) override
{
// Audio thread - push to queue
mMidiQueue . Push (msg);
}
void OnIdle () override
{
// Main thread - pop and process
IMidiMsg msg;
while ( mMidiQueue . Pop (msg))
{
// Update UI based on MIDI
if ( msg . StatusMsg () == IMidiMsg ::kNoteOn)
{
// Highlight key, etc.
}
}
}
private:
IPlugQueue < IMidiMsg > mMidiQueue;
};
ISender System
The ISender classes use IPlugQueue internally for audio visualization:
template < int MAXNC = 1 , int QUEUE_SIZE = 64 , typename T = float>
class ISender
{
public:
void PushData ( const ISenderData < MAXNC , T > & d ) // Audio thread
{
mQueue . Push (d);
}
void TransmitData ( IEditorDelegate & dlg ) // Main thread
{
while ( mQueue . ElementsAvailable ())
{
ISenderData < MAXNC, T > d;
mQueue . Pop (d);
dlg . SendControlMsgFromDelegate ( d . ctrlTag , kUpdateMessage,
sizeof (d), & d);
}
}
protected:
IPlugQueue < ISenderData < MAXNC, T >> mQueue {QUEUE_SIZE};
};
See Visualization for detailed usage.
Memory Management
Pre-Allocate in OnReset
Allocate all buffers in OnReset(), never in ProcessBlock():
class MyPlugin : public Plugin
{
public:
void OnReset () override
{
// Main thread - safe to allocate
const int maxBlockSize = GetBlockSize ();
mWorkBuffer . Resize (maxBlockSize * 2 );
mDelayBuffer . Resize ( GetSampleRate () * 2 ); // 2 seconds
}
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// Audio thread - use pre-allocated buffers
sample * pWork = mWorkBuffer . Get ();
sample * pDelay = mDelayBuffer . Get ();
// Process without allocation...
}
private:
WDL_TypedBuf < sample > mWorkBuffer;
WDL_TypedBuf < sample > mDelayBuffer;
};
WDL_TypedBuf is iPlug2’s dynamic array. Resize() may allocate, so only call it outside ProcessBlock().
Stack vs Heap
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// ✅ Stack allocation - realtime safe for small, fixed sizes
sample temp [ 512 ];
if (nFrames <= 512 )
{
// Use temp buffer...
}
}
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// ❌ Heap allocation - NOT realtime safe
sample * temp = new sample [nFrames]; // Can block!
// Process...
delete[] temp; // Also can block!
}
OnMessage for Bidirectional Communication
Send arbitrary data from UI to DSP:
// UI thread - send data
pGraphics -> GetDelegate ()-> SendArbitraryMsgFromUI (
kMsgTagSliderChanged, kCtrlTag, sizeof (data), & data
);
// DSP thread - receive data
bool IPlugChunks :: OnMessage ( int msgTag , int ctrlTag ,
int dataSize , const void* pData ) override
{
if (msgTag == kMsgTagSliderChanged)
{
auto * pVals = reinterpret_cast < const double *> (pData);
memcpy (mSteps, pVals, kNumSteps * sizeof ( double )); // Copy to DSP state
return true ;
}
return false ;
}
Messages sent via SendArbitraryMsgFromUI() are delivered on the audio thread . Keep OnMessage() fast and realtime-safe.
Common Threading Patterns
Pattern 1: Parameter Changes
// OnParamChange runs on MAIN thread
void OnParamChange ( int paramIdx ) override
{
double value = GetParam (paramIdx)-> Value ();
// Store atomically for audio thread
mAtomicParam . store (value, std ::memory_order_release);
}
// ProcessBlock runs on AUDIO thread
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// Read atomically
double param = mAtomicParam . load ( std ::memory_order_acquire);
// Use param...
}
Pattern 2: Audio → UI Visualization
// ProcessBlock - AUDIO thread
void ProcessBlock ( sample ** inputs , sample ** outputs , int nFrames ) override
{
// Process audio...
// Push to lock-free queue
mSender . ProcessBlock (outputs, nFrames, kCtrlTag);
}
// OnIdle - MAIN thread (called ~60Hz)
void OnIdle () override
{
// Pop from queue and send to UI
mSender . TransmitData ( * this );
}
Pattern 3: UI → Audio Messages
// UI action - MAIN thread
button -> SetActionFunction ([]( IControl * pCaller ) {
pCaller -> GetDelegate ()-> SendArbitraryMsgFromUI (
kMsgTrigger, kNoTag, 0 , nullptr
);
});
// OnMessage - AUDIO thread
bool OnMessage ( int msgTag , int ctrlTag , int dataSize , const void* pData ) override
{
if (msgTag == kMsgTrigger)
{
TriggerEnvelope (); // Realtime-safe operation
return true ;
}
return false ;
}
Debugging Threading Issues
Common symptoms:
Clicks/Pops
Crashes
Deadlocks
Missed Updates
Cause : Blocking operation on audio threadFix : Remove allocations, locks, I/O from ProcessBlock()
Cause : Race condition or non-atomic accessFix : Use atomics or lock-free queues
Cause : Mutex held across threadsFix : Never use mutexes between audio and main threads
Cause : Queue overflowFix : Increase queue size or reduce message rate
Thread Sanitizer
Use ThreadSanitizer to detect data races:
# Compile with thread sanitizer
export CXXFLAGS = "-fsanitize=thread -g"
make
# Run plugin in host
# TSan will report data races
Best Practices
Identify Thread Context
Know which thread your code runs on:
ProcessBlock(), OnMessage() → Audio
OnIdle(), UI callbacks → Main
OnReset() → Main (usually)
Use Appropriate Primitives
Simple values → std::atomic
Complex data → IPlugQueue
UI updates → ISender classes
Pre-Allocate Memory
Allocate all buffers in OnReset() or constructor
Test Under Load
Test with small buffer sizes (32 samples) at high sample rates (96kHz)
See Also
When in doubt, use IPlugQueue. It’s lock-free, tested, and handles most audio-to-UI communication needs.