Real-Time Patterns¶
Lock-free communication patterns for GUI โ Audio thread interaction.
๐ฏ Patterns Included¶
1. DoubleBuffer - Settings Updates¶
Use when: Frequent updates of small data structures (settings, parameters)
Pattern:
Writer (GUI): Reader (Audio):
1. Get write buffer 1. Get read buffer (wait-free!)
2. Modify 2. Use data
3. Publish (swap) 3. No blocking
Example:
struct Settings {
float gain;
float pan;
};
DoubleBuffer<Settings> settings;
// GUI thread
auto& write = settings.getWriteBuffer();
write.gain = 0.5f;
settings.publish(); // Atomic swap
// Audio thread
const auto& read = settings.getReadBuffer(); // Always valid
process(read.gain);
Properties: - โ Wait-free reads - โ Lock-free writes - โ 2x memory overhead - โ Single writer, single reader
2. CommandQueue - Discrete Actions¶
Use when: Infrequent discrete actions (preset load, reset, MIDI events)
Pattern:
Producer (GUI): Consumer (Audio):
1. Create command 1. Pop all commands
2. Push to queue 2. Execute each
3. Repeat 3. Process audio
Example:
CommandQueue<256> queue;
// GUI thread
queue.push(Command::setParameter(0, 0.5f));
queue.push(Command::loadPreset(42));
// Audio thread
queue.processAll([](const Command& cmd) {
switch (cmd.type) {
case Command::Type::SetParameter:
setParam(cmd.param, cmd.value);
break;
case Command::Type::LoadPreset:
loadPreset(cmd.param);
break;
}
});
Properties: - โ Lock-free SPSC queue - โ Fixed capacity (no allocations) - โ Cache-friendly ring buffer - โ ๏ธ Bounded (can be full)
3. RCUPointer - Large Data Updates¶
Use when: Infrequent updates of large data (impulse responses, lookup tables)
Pattern:
Readers (Audio): Writer (GUI):
1. Load pointer 1. Clone current data
2. Use data 2. Modify clone
3. Zero cost! 3. Swap pointer
4. Delete old (deferred)
Example:
struct IRData {
std::vector<float> samples;
};
RCUPointer<IRData> ir_data;
// GUI thread
ir_data.update([](IRData& data) {
// Modify copy
data.samples = loadNewIR();
});
ir_data.collect(); // Clean up old versions
// Audio thread
const IRData* ir = ir_data.read(); // Just pointer load!
process(ir->samples);
Properties: - โ Zero-cost reads (wait-free) - โ Writers don't block readers - โ ๏ธ Copy-on-write (expensive for large data) - โ ๏ธ Multiple versions in memory - โ ๏ธ Deferred deletion (grace period)
๐ Usage Guide¶
When to Use What?¶
| Pattern | Update Frequency | Data Size | Use Case |
|---|---|---|---|
| DoubleBuffer | High (>100 Hz) | Small | Parameter smoothing |
| CommandQueue | Low (<10 Hz) | Tiny | Preset changes, MIDI |
| RCUPointer | Very low (<1 Hz) | Large | IR data, lookup tables |
Real-World Example¶
class AudioProcessor {
// Fast parameter updates
DoubleBuffer<Settings> settings_;
// Discrete actions
CommandQueue<256> commands_;
// Large data updates
RCUPointer<IRData> ir_data_;
public:
// GUI thread
void setParameter(int id, float value) {
auto& write = settings_.getWriteBuffer();
// Update parameter...
settings_.publish();
}
void loadPreset(int id) {
commands_.push(Command::loadPreset(id));
}
void loadIR(const std::vector<float>& samples) {
ir_data_.update([&](IRData& data) {
data.samples = samples;
});
}
// Audio thread
void process(float* buffer, int frames) {
// 1. Process commands
commands_.processAll([this](const Command& cmd) {
handleCommand(cmd);
});
// 2. Get latest settings
const auto& settings = settings_.getReadBuffer();
// 3. Get IR data (if needed)
const IRData* ir = ir_data_.read();
// 4. Process audio
for (int i = 0; i < frames; ++i) {
buffer[i] *= settings.gain;
}
}
};
๐งช Testing¶
Or manually:
Test Coverage¶
- โ DoubleBuffer (basic + threaded)
- โ CommandQueue (push/pop, processAll, threaded)
- โ RCUPointer (basic + threaded + garbage collection)
- โ Real-world scenario (GUI + Audio threads, all patterns)
๐ Performance¶
DoubleBuffer¶
CommandQueue¶
RCUPointer¶
โ ๏ธ Common Pitfalls¶
1. Wrong Pattern Choice¶
// โ BAD: DoubleBuffer for large data
DoubleBuffer<std::vector<float>> huge_data(1'000'000);
// Wastes 2x 1M floats = 8 MB
// โ
GOOD: RCUPointer for large data
RCUPointer<std::vector<float>> huge_data;
// Only copies when updating (rare)
2. Forgetting to Publish¶
// โ BAD: Forget to publish
auto& write = buffer.getWriteBuffer();
write.gain = 0.5f;
// Reader never sees update!
// โ
GOOD: Always publish
buffer.publish();
3. Queue Overflow¶
// โ BAD: Ignore push failure
queue.push(cmd); // Silently fails if full
// โ
GOOD: Check return value
if (!queue.push(cmd)) {
// Handle overflow
}
// OR: Use assertion for debugging
queue.pushOrAssert(cmd);
4. RCU Grace Period¶
// โ BAD: Never collect garbage
rcu_ptr.update(new_data); // Old data never deleted (leak!)
// โ
GOOD: Collect periodically
rcu_ptr.update(new_data);
rcu_ptr.collect(); // Call from writer thread
๐ References¶
- Double Buffering: https://gameprogrammingpatterns.com/double-buffer.html
- Lock-Free Queues: https://www.boost.org/doc/libs/1_76_0/doc/html/lockfree.html
- RCU: https://en.wikipedia.org/wiki/Read-copy-update
- Memory Ordering: https://en.cppreference.com/w/cpp/atomic/memory_order
๐ License¶
Part of AudioLab Core library.